Android & iOS App Automation via One test case using Appium

Agenda of this blog is to come up with a solution to use only one test case and execute it on both android and iOS. Lets first focus on why we would need something like this.

First Lets try to answer why Organisations launch their apps on multiple platforms. Reason is: When an Organisation plans to launch an app, their main objective is to reach out to maximum users. So they decide to launch their apps on most popular platforms. iOS and Android are two most popular platform today.Thats the main reason why organisation simultaneously develop and launch apps on both platforms.

Now Lets focus on what it means for Mobile Dev and Mobile Test Teams.Dev teams can decide either of the following options:

1. Start development of iOS and android separately and have two teams do this.
2. Decide to use cross platform app development tools to write only one piece of code and deploy it on both android and iOS.

Both options has their own pros and cons but one main benefit of option 2 is, it results in drastically reduced development time and budget. ( Some of you may not agree with this, but this is not the main agenda of this blog)

Now can Test Teams also follow option 2??? 

So that test time and budget required for automating both apps is not 200%. This was the question that was hovering in mind from a long time. So i decided to give it a try to find a solution.

Lets try to understand whats needed to automate either a website or a mobile app. Irrespective of the tool used, we need to have these two facilities available to us:

1. How to find an element
2. How to interact with an element
So whats the Solution. Solution is to find a tool that can give us both of these for both the platforms. Thus only tool that can be used here is –  Appium (No points for guessing). The main advantage of Appium is that its supports multiple platforms: Android, iOS and firefox. Our solution lies in to understand how Appium supports multiple platforms. To understand “how Appium supports multiple platforms“, we need to understand Appium’s architecture. Please observe the below image carefully.

Appium_architecture

Let me quote some text from Appium’s Documentation:   [“Appium is at its heart a webserver that exposes a REST API. It receives connections from a client, listens for commands, executes those commands on a mobile device, and responds with an HTTP response representing the result of the command execution. The fact that we have a client/server architecture opens up a lot of possibilities:”]

When we start appium, we pass some desired capabilities to appium session, which basically tells appium to start  either android or iOS session. For example, if i pass desired capabilities like “platformName” ==”iOS”, “deviceName”==”iPhone Simulator” or “platformName” ==”Android”, “deviceName”==”Android Emulator” then appium would start iOS and android session respectively.

Now when we run a test case, depending on which appium session we have started, all the commands passed to Appium Server will be converted to that platform’s UI Automation native commands. This is how multiple platforms are supported.

Simple!! isn’t it?? 🙂

Now Lets see how Appium helps us in achieving the point 2 mentioned above. Since Appium extends Selenium Webdriver so interaction with elements is independent of the platform. For Example a button will be a button on android and iOS both and the function to interact with a button is .click() irrespective of platform. similarly for textbox, the function is .sendKeys() on both platforms.

So the only point left is point 1. Here Appium cant help us much because how we find elements is dependent on platform. On iOS, locators used are Name and Xpath and for Android locators used are id and ClassName etc. This issue was resolved with the help of framework where depending on which platform we want to automate, we loaded the required locators at run time.

We created two .properties files and all the locators specific to one platform were stored in one property file and specific to other were stored in other property file. Based on for which platform we want to execute the test for, locators from one of the feature files are loaded.

Lets go through the Framework and understand how we do this. Here is the tech stack for this framework:

Ant ( Build Tool), Java ( Programming Language), Appium(Automation Tool), TestNG(Test Framework) and Page Objects (Test Design Pattern)

1. In build.xml, apart from defining normal TestNG specific tasks and targets, we have defined two targets run-test-android and run-test-iOS which not only runs android and iOS test suits but also sets the value of variable “Browser” to android and iOS respectively.

<?xml version="1.0" encoding="UTF-8"?>

<project name="AppiumAndroidiOS" default="" basedir=".">

<property name="sourceDir" location="src" />
<property name="buildDir" location="bin" />
<property name="libDir" location="lib" />
<property name="config" location="config" />
<property name="outputDir" location="testsuite-output" />
<property name="testSuiteLocation" location="testSuite" />

<!-- @purpose: declare a property set to be sent to Java Code via testng task -->
<propertyset id="propset1">
<propertyref name="browser"/>
</propertyset>

<!-- setting up the master Classpath -->
<path id="master-classpath">
<fileset dir="${libDir}">
<include name="*.jar"></include>
<include name="**/*.jar"></include>
</fileset>
<pathelement path="${buildDir}"></pathelement>
<dirset dir="${config}"/>
</path>

<path id="properties">
<dirset dir="src"/>
</path>

<!-- @ Purpose: set browser value for iOS -->
<target name="set-browser-iOS">
<property name="browser" value="iOS"/>
</target>

<!-- @ Purpose: set browser value for android -->
<target name="set-browser-android">
<property name="browser" value="android"/>
</target>

<target name="clean" description="Remove build and output directories">
<delete dir="${buildDir}" />
<delete dir="${outputDir}" /> 
<delete dir="${logsDir}" /> 
</target>

<target name="build" description="Creates a build of the test suite.">
<echo>"Making directory ${buildDir}"</echo> 
<mkdir dir="${buildDir}" />
<echo>"Making directory ${outputDir}"</echo> 
<mkdir dir="${outputDir}" />
<echo>"Doing build..."</echo> 

<javac srcdir="${sourceDir}" destdir="${buildDir}" classpathref="master-classpath" debug="true" deprecation="false" failonerror="true" fork="false" includeantruntime="false"/>
</target>


<taskdef resource="testngtasks" classpath="${libDir}/testng-6.8.jar" />
<!--<taskdef resource="testngtasks" classpath="${libDir}/testng-5.9-jdk15.jar" />-->

<target name="do-test-iOS" description="Execute TestNG tests">
<testng classpathref="master-classpath" workingDir="${buildDir}" outputdir="${outputDir}" suitename="jabong-test-suite-iOS" > 
<!-- refer the property set to java code to pass the value of browser property-->
<propertyset refid="propset1"/>
<xmlfileset dir="${testSuiteLocation}" includes="iOS-testSuite.xml" /> 
</testng>
</target>

<target name="do-test-android" description="Execute TestNG tests">
<testng classpathref="master-classpath" workingDir="${buildDir}" outputdir="${outputDir}" suitename="jabong-test-suite-android" > 
<!-- refer the property set to java code to pass the value of browser property-->
<propertyset refid="propset1"/>
<xmlfileset dir="${testSuiteLocation}" includes="android-testSuite.xml" /> 
</testng>
</target>

<target name="run-test-iOS" depends="clean,build,set-browser-iOS,do-test-iOS"></target>

<target name="run-test-android" depends="clean,build,set-browser-android,do-test-android"></target>

</project>

2. DriverFactory.java, once ant target is executed in command line , as per TestNG, control will come to function tagged with @BeforeSuite. In this function, first i check the value of system property “Browser” that was being set in build.xml. if it is android or iOS, Appium Server server session is started accordingly.

package com.appium.helper;

import io.appium.java_client.AppiumDriver;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.TimeUnit;

import org.openqa.selenium.Platform;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.support.ui.WebDriverWait;

import org.testng.TestNG;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeSuite;

public class DriverFactory {

public static AppiumDriver driver = null;
public static WebDriverWait waitVar = null;
public static int waitTime=20;

@SuppressWarnings( "deprecation" )
final String outputDIR=TestNG.getDefault().getOutputDirectory();
public String NewFileNamePath = null;
public String className = null;
public String testName = null;

public static String browser="";
public static Platform currentOS = Platform.getCurrent();
public static String propertyFileName = null;

@BeforeSuite( alwaysRun=true )
public static void getDriverInstance() throws Exception
{
System.out.println("Inside Before Suite");

browser=System.getProperty("browser");

System.out.println("Browser= "+browser);
System.out.println("Platform= "+currentOS);

createDriver(browser);
}

public static void createDriver(final String browserId) throws Exception {
System.out.println("inside setup method");

if(browserId.equalsIgnoreCase("iOS")){
returnIOSDriver();
propertyFileName = "iOS";
}
if(browserId.equalsIgnoreCase("android")){
returnAndroidDriver();
propertyFileName = "android";
}

waitVar= new WebDriverWait( driver, waitTime );
driver.manage().timeouts().implicitlyWait(waitTime, TimeUnit.SECONDS);

}

public static void returnIOSDriver() throws MalformedURLException{
DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities
.setCapability(
"app",
"/Users/Apple/Library/Developer/Xcode/DerivedData/PayCloud-ffksktfojvhzadaztzhtikjpdhdv/Build/Products/Debug-iphonesimulator/PayCloud.app");
capabilities.setCapability(CapabilityType.BROWSER_NAME, "iOS");
capabilities.setCapability("platformVersion", "7.1");
capabilities.setCapability("platformName", "iOS");
capabilities.setCapability("deviceName", "iPhone Simulator");
driver = new AppiumDriver(new URL("http://127.0.0.1:4723/wd/hub"),
capabilities);

waitVar = new WebDriverWait(driver, 90);
System.out.println("iOS started");
}

public static void returnAndroidDriver() throws MalformedURLException{
// set up appium
final File classpathRoot = new File(System.getProperty("user.dir"));
final File appDir = new File(classpathRoot, "../resources/");
final File app = new File(appDir, "AndroidAppiumPayCloud.apk");

final DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("platformName", "Android");
capabilities.setCapability("deviceName", "Android Emulator");
capabilities.setCapability("platformVersion", "4.4");
capabilities.setCapability("app", app.getAbsolutePath());

driver = new AppiumDriver(new URL("http://127.0.0.1:4723/wd/hub"),
capabilities);

waitVar = new WebDriverWait(driver, 90);
System.out.println("android started");
}

@AfterSuite( alwaysRun=true)
public void tearDown() throws Exception {
//Close the app and simulator
System.out.println("Inside quit");
driver.quit();
}

}

3. Next Step is Page Objects. We have defined a page object for each page of our mobile app. But there is a twist in the constructor. Here also in the constructor i check the value of system property “Browser” that was being set in build.xml. if it is android or iOS, locators from properties files are loaded accordingly.
HomePage.java

package com.appium.pages;

import io.appium.java_client.AppiumDriver;

import org.openqa.selenium.By;
import org.openqa.selenium.support.ui.ExpectedConditions;

import com.appium.helper.DriverFactory;
import com.appium.utils.PropertyFileReader;

import static org.junit.Assert.*;

public class HomePage extends DriverFactory{

PropertyFileReader prop = new PropertyFileReader();

By signInButton = null;
By userName = null;
By password = null;
By done = null;

public HomePage(AppiumDriver driver) throws InterruptedException{
Thread.sleep(5000);

PropertyFileReader prop = new PropertyFileReader();
if(propertyFileName == "android"){
signInButton = By.id(prop.returnPropVal("signInButton"));
userName = By.id(prop.returnPropVal("userName"));
password = By.id(prop.returnPropVal("password"));
}
if(propertyFileName == "iOS"){
signInButton = By.xpath(prop.returnPropVal("signInButton"));
userName = By.xpath(prop.returnPropVal("userName"));
done = By.name(prop.returnPropVal("done"));
password = By.xpath(prop.returnPropVal("password"));
}
waitVar.until(ExpectedConditions.elementToBeClickable(signInButton));

assertTrue(driver.findElement(signInButton).isEnabled());

}

public void login(String Username, String Password){
//enter data in username and password
driver.findElement(userName).clear();
driver.findElement(userName).sendKeys(Username);
driver.findElement(password).clear();
driver.findElement(password).sendKeys(Password);
if(propertyFileName == "iOS"){
//click done to get the focus back to main window
driver.findElement(done).click();
waitVar.until(ExpectedConditions.presenceOfElementLocated(signInButton));
}
//click login
driver.findElement(signInButton).click();
}

}

4. As mentioned above, We had to deal with different locators for different platforms. Functions required to interact with elements are platform independent so there is no change in how functions are defined.

5. So now if we see our Test cases, these are normal test cases which have functions of page objects(like normal page object pattern framework test cases). Everything is handled in build.xml, DriverFactory.java and PageObjects so there is nothing left for Test Cases.

TestAndroidiOS.java

package com.appium.test;

import org.testng.annotations.Test;

import com.appium.helper.DriverFactory;
import com.appium.pages.HomePage;
import com.appium.pages.PaymentPage;
import com.appium.pages.SwipePage;
import com.appium.pages.TransactionComplete;

public class TestandroidiOS extends DriverFactory{

@Test(groups={"android","iOS"},alwaysRun=true)
public void testandroidcase() throws InterruptedException{

System.out.println("test case started");
HomePage hp = new HomePage(driver);
hp.login("Shankar", "Shankar");

PaymentPage pp = new PaymentPage(driver);
pp.enterCheckoutAmount("2");
pp.clickCheckout();

SwipePage sp = new SwipePage(driver);
sp.clickPay();

TransactionComplete tc = new TransactionComplete(driver);
tc.verifyTransactionValue("$2.0");

System.out.println("test case ended");
}

}

I hope after reading this blog, your testing time for automating an app ( which is on both android and iOS would come down).

PS: I understand that an app on android and iOS can not be 100% replica because of some platform related changes. but this blog and this framework is an attempt to reduce the testing time for those parts of the app which are similar.

Advertisements

2 thoughts on “Android & iOS App Automation via One test case using Appium

  1. Hello Shankar,

    Do you have any test App for this scenario ? which can be executed in both Android & iOS ?
    Plz share App details so that i can show a demo to my Manager. Thanks for spending lot of your time on ur blog. Keep up doing this sir 🙂

    Thanks

    Like

    • Hi Mallikarjunareddy,

      Glad you liked the blog, But i am afraid i have used apps which are xebia property and cant be shared on open Platform. If you really need to demo this to anyone, please take any two apps from Appium Source Code (one for ios and one for android) and show the concept of using the locators dynamically. if someone gets the concept than if we are using same app on both platforms or different apps that hardly matters.

      Liked by 1 person

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s