Android App Automation Complete Guide With Java, Appium 2, Gradle, TestNG and Android Emulator For Beginner

Darmawan Hadiprasetyo
15 min readMar 4, 2023

--

Prerequisite:

Appium 2.x (https://appium.io/)
Selenium (https://www.selenium.dev/)
Appium inspector (https://github.com/appium/appium-inspector/releases)
Intellij (Or use any IDE you like that supports Java language programming)
Android Studio (https://developer.android.com/)
Java (https://www.oracle.com/id/java/technologies/downloads/)
TestNG (https://testng.org/doc/documentation-main.html)

Setup Android Studio

Download and install Android Studio based on your preferred OS. Then set your ANDROID_HOME by follow this steps:

  1. Mac user
  • Open terminal and enter following command:
nano ~/.bash_profile
  • Add this lines:
export ANDROID_HOME=path-to-your-android-sdk
export PATH=$PATH:$ANDROID_HOME/platform-tools
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/tools/bin
export PATH=$PATH:$ANDROID_HOME/emulator
  • Replace path-to-your-android-sdk with Android SDK Location which you have copied from Android Studio SDK Manager.
  • Control+x and save.
  • Test your current configuration by entering following command into terminal and make sure it shows all the SDK details such as Android Debug Bridge version, etc:
adb

2. Windows user:

  • Follow instruction on this blog post by Tarun Goswami (click here).

Setup Java

  1. Download and install Java based on your preferred OS.
  2. Set your JAVA_HOME. Follow this instruction by Baeldung (click here).

Setup Appium

sudo npm install -g appium@next
  1. Open your terminal or cmd then enter above command and wait for installation to finish.

2. Install Appium 2 driver for Android using terminal (Read more about Appium 2 installation here).

appium driver install uiautomator2

3. Check if uiautomator2 driver is installed.

appium driver list

4. Download and install Appium Inspector based on your preferred OS.

Setup Android Emulator

  1. Open Android Studio -> More Actions -> Virtual Device Manager -> Create device -> Choose any type of device you want (in this tutorial I use Pixel 4 with Google Play) -> Choose your Android version which we have installed in previous steps (API 33 with Google Play).

2. Run your previously created Android emulator.

Connect Appium Inspector to Appium Server

  1. Start Appium server using terminal by entering following command:
appium --address 127.0.0.1 --port 4723

2. Connect Appium Inspector to your Appium Server

Set Driver Capabilities

In order for Appium to be able to connect to your device and app, you need to fill in some information about your testing environment such as device and application information.

  1. Find your Android emulator UDID by using terminal and enter this command:
adb devices
  • emulator-5554 is the udid of my Android emulator.

2. Copy this JSON capabilities into your Appium Inspector

{
"platformName": "Android",
"appium:udid": "yourudid",
"appium:appPackage": "com.google.android.youtube",
"appium:appActivity": "com.google.android.youtube.app.honeycomb.Shell$HomeActivity",
"appium:deviceName": "yourdevicename",
"appium:automationName": "UiAutomator2",
"appium:platformVersion": "13",
"appium:autoGrantPermissions": true
}
  • Replace “yourudid” in “appium:udid” with your UDID which you found in this previous step.
  • Replace “yourdevicename” in “appium:deviceName” with you Android emulator name. You can find information about your device name in Android Studio -> Device Manager.
  • Pixel 4 API 33 is the name of my emulator device.
  • Actually there are 2 ways to set which application you will automate:
  • (1) By using capability of “appium:app”, with this your apk is not pre-installed on your device and will be installed each time your test run. If you are using this type of capability then you don’t need to use “appium:appPackage” and “appium:appActivity” as we used in the previous steps. Your JSON capabilities will look like this:
{
"platformName": "Android",
"appium:udid": "yourudid",
"appium:app": "/path/to/your/app.apk",
"appium:deviceName": "yourdevicename",
"appium:automationName": "UiAutomator2",
"appium:platformVersion": "13",
"appium:autoGrantPermissions": true
}
  • (2) By using capabilities of “appium:appPackage” and “appium:appActivity”. When using this, it means that your app must be pre-installed in your device before your test run.
  • (2.1) Easiest way to find your app package and app activity:
  • (2.1.1) Install “Package Name Viewer 2.0” from Play Store
  • (2.1.2) Open “Package Name Viewer 2.0” -> find, open and copy the detail of your application (In this tutorial I am going to use YouTube as app to automate).
app:YouTube
package:com.google.android.youtube (use this as value in appium:appPackage)
Launcher:com.google.android.youtube.app.honeycomb.Shell$HomeActivity (use this as value in appium:appActivity)

So your JSON capabilities will look like this:

{
"platformName": "Android",
"appium:udid": "yourudid",
"appium:appPackage": "com.google.android.youtube",
"appium:appActivity": "com.google.android.youtube.app.honeycomb.Shell$HomeActivity",
"appium:deviceName": "yourdevicename",
"appium:automationName": "UiAutomator2",
"appium:platformVersion": "13",
"appium:autoGrantPermissions": true
}

3. Click Start Session and wait until application is opened by Appium.

4. Read more about what capabilities you can set here and here.

Start Inspecting Element

Android application interface is consist of elements. In order to automate our app, we first need to know the location of the element which we will perform actions such as clicking, typing, swiping, etc later.

The purpose of inspecting element is to find the location of element and simulate actions you would like to perform.

  1. After session started in Appium Inspector, simply hover and tap on the element you would like to inspect. For example we will be inspecting search icon element:

2. After tapping on the element, Appium Inspector will show the detail of the element on the right side of Appium Inspector (see red box):

3. Using the given detail on Appium Inspector, we can use that information to locate element later in our code.

  • Appium Inspector provides many details of the element we are inspecting, and we can use this information in our code as selector strategies to find this element. We can find element by using those attributes and values. Let’s take a look at the Java code example:
// Using resource-id
driver.findElement(AppiumBy.id("com.google.android.youtube:id/menu_item_view"))
  • You can learn more about the selector strategies here.
  • Appium Inspector really makes our life easier by providing the Find By section in the Selected Element’s section.
  • In previous steps we have learned how to locate element by using accessibility-id. But in reality, most of the times we need to use Xpath and customize it.
  • To understand about Xpath easily, the usage of Xpath is similar to path expression with traditional computer system. You can put as many parents as needed to correctly find your element.
parent1/parent2/parent3/your-element
// Sample code using Xpath
WebElement el = driver.findElement(AppiumBy.xpath("//android.widget.ImageView[@content-desc="Search"]"))
  • Before we continue, please read more about Xpath here.

Perform actions in Appium Inspector

The next step is we need to understand about the actions that we can performs in our code. By using Appium Inspector we can simulate those actions before we implement it in our code.

Let’s use the help of recording tools in our Appium Inspector which will give us sample code of our actions. Click the eye button on your Appium Inspector header.

  1. Tap/click action
  • Click Tap icon on Selected Element’s section
  • After you perform Tap action on Appium Inspector, the action will be applied to your app on your emulator as well and the Recorder section will give you sample code of the action you performed. You can choose many sample of programming languages as well.
// Let's understand what the action do in code
// First you need to find the element and assign it to a variable
WebElement el3 = driver.findElement(AppiumBy.accessibilityId("search"))
// After element found then you can perform click action on that element
el3.click()

2. Send keys action

  • On Appium Inspector, inspect the search bar to retrieve the element’s attribute.
  • Now click the Send Keys button on Selected Element’s section and let’s look for “Rick Rolled (Short Version)” lol.
// Let's understand what the action do in code
// First you need to find the element and assign it to a variable
WebElement el4 = driver.findElement(AppiumBy.id("com.google.android.youtube:id/search_edit_text"))
// After element found then you can perform send keys action on that element
el4.sendKeys("Rick Rolled (Short Version)")
  • Inspect the search suggestion.
  • If we take a look at the Xpath suggestion, I think it’s way too long and won’t be a good practice if we use the absolute Xpath. So by using the Attribute information, we could do the following things to minimize our Xpath:
// xpath = '//class[@what-attribute="value-of-attribute"]'
// example:
String newXpath = '//android.widget.TextView[@text="rick rolled (short version)"]'
// perform tap action using our new xpath
WebElement el3 = driver.findElement(AppiumBy.xpath(newXpath))
el3.click()
  • Now try to inspect and perform actions using Appium Inspector until a video is played.

TestNG

  1. What is TestNG?

From TestNG official documentation:

TestNG is a testing framework designed to simplify a broad range of testing needs, from unit testing (testing a class in isolation of the others) to integration testing (testing entire systems made of several classes, several packages and even several external frameworks, such as application servers).

Writing a test is typically a three-step process:

- Write the business logic of your test and insert TestNG annotations in your code.

- Add the information about your test (e.g. the class name, the groups you wish to run, etc…) in a testng.xml file or in build.xml.

- Run TestNG.

2. TestNG Before or After Annotations

TestNG test can be configured by annotations such as @BeforeXXX and @AfterXXX which allows to perform some Java logic before and after a certain point.

Annotations usage:

Suite level:

Let’s say you have a test suite file named testng.xml that contains all your automation tests. By using this annotation then all methods annotated with these following two will be run before/after your all your tests in that test suite are executed.

  • @BeforeSuite: The annotated method will be run before all tests in this suite have run.
  • @AfterSuite: The annotated method will be run after all tests in this suite have run.

Test level:

By using this annotation then all methods annotated with these following two will be run before/after all your test method/class is executed.

public class LoginTest {
@BeforeTest
public void doBeforeTest(){
// this will be executed before your first test start (either testUserLogin() or testUserLogout(), this will only run once)
}
@AfterTest
public void doAfterTest(){
// this will be executed after your last test is finished
}
@Test
public void testUserLogin(){
// do something
}
@Test
public void testUserLogout(){
// do something
}
}
  • @BeforeTest: The annotated method will be run before any test method belonging to the classes inside the <test> tag is run.
  • @AfterTest: The annotated method will be run after all the test methods belonging to the classes inside the <test> tag have run.

Group level:

  • @BeforeGroups: The list of groups that this configuration method will run before. This method is guaranteed to run shortly before the first test method that belongs to any of these groups is invoked.
  • @AfterGroups: The list of groups that this configuration method will run after. This method is guaranteed to run shortly after the last test method that belongs to any of these groups is invoked.

Class level:

public class LoginTest {
@BeforeClass
public void doBeforeClass(){
// this will be executed before your class LoginTest start
}
@AfterClass
public void doAfterClass(){
// this will be executed after class LoginTest is finished
}
@Test
public void testUserLogin(){
// do something
}
@Test
public void testUserLogout(){
// do something
}
}
  • @BeforeClass: The annotated method will be run before the first test method in the current class is invoked.
  • @AfterClass: The annotated method will be run after all the test methods in the current class have been run.

Method level:

public class LoginTest {
@BeforeMethod
public void doBeforeMethod(){
// this will be executed before each method start
}
@AfterMethod
public void doAfterMethod(){
// this will be executed after each method is finished
}
@Test
public void testUserLogin(){
// do something
}
@Test
public void testUserLogout(){
// do something
}
}
  • @BeforeMethod: The annotated method will be run before each test method.
  • @AfterMethod: The annotated method will be run after each test method.

TestNG guarantees that the “@Before” methods are executed in inheritance order (highest superclass first, then going down the inheritance chain), and the “@After” methods in reverse order (going up the inheritance chain).

If you are still confused about the order execution please take a look of this example here.

3. TestNG Test Annotation

  • @Test: Marks a class or a method as part of the test.
public class LoginTest {
public void thisIsNotTest(){
// TestNG will not execute this as a test
}
@Test
public void testUserLogout(){
thisIsNotTest();
// TestNG will execute this as a test
}
}

Before we continue please learn more about @Test annotations here.

4. TestNG Suite

You can group all your test you created into one XML file and execute those tests as a suite.

Here are some example of how you can configure your XML file:

  • By class names: below example will run all tests within ParameterSample class which located in test.sample package.
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >

<suite name="Suite1" verbose="1" >
<test name="Nopackage" >
<classes>
<class name="NoPackageTest" />
</classes>
</test>

<test name="Regression1">
<classes>
<class name="test.sample.ParameterSample"/>
<class name="test.sample.ParameterTest"/>
</classes>
</test>
</suite>
  • By package names: below example will run all test classes within test.sample package.
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >

<suite name="Suite1" verbose="1" >
<test name="Regression1" >
<packages>
<package name="test.sample" />
</packages>
</test>
</suite>
  • By groups with include and exclude: below example will exclude group test with name brokenTests but include group test with name checkinTests.
<test name="Regression1">
<groups>
<run>
<exclude name="brokenTests" />
<include name="checkinTests" />
</run>
</groups>
</test>
public class LoginTest {
@Test (groups = { "checkinTests" })
public void thisCheckInUser(){
// All test with groups checkinTests will be executed
}
@Test (groups = { "brokenTests" })
public void testToBroken(){
thisIsNotTest();
// All test with groups brokenTests will not be executed
}
}
  • By test method with include and exclude: below example will execute testMethod() within test.IndividualMethodsTest.
<test name="Regression1">
<classes>
<class name="test.IndividualMethodsTest">
<methods>
<include name="testMethod" />
</methods>
</class>
</classes>
</test>

Assertion

The purpose of doing automation testing is to verify either our test pass or fail. To decide a test to pass or fail we need to use assertion to see if our test has met the expected condition.

Commonly use assertions:

  • Assert.assertEqual(String actual, String expected): Pass the actual string value and the expected string value as parameters. Validates if the actual and expected values are the same or not.
  • Assert.assertEqual(String actual, String expected, String message): Similar to the previous method just that when the assertion fails, the message displays along with the exception thrown.
  • Assert.assertEquals(boolean actual, boolean expected): Takes two boolean values as input and validates if they are equal or not.
  • Assert.assertTrue(condition): This method asserts if the condition is true or not. If not, then the exception error is thrown.
  • Assert.assertTrue(condition, message): Similar to the previous method with an addition of message, which is shown on the console when the assertion fails along with the exception.
  • Assert.assertFalse(condition): This method asserts if the condition is false or not. If not, then it throws an exception error.
  • Assert.assertFalse(condition, message): Similar to the previous method but with an addition of a message string which is shown on the console when the assertion fails, i.e., the condition is true*.

Let’s Code

  1. Create new Java project with Gradle in Intellij.
  2. Under src/test/java create a new Java class.

3. Search for TestNG, Appium 2 and Selenium dependencies on Maven Repository and add them to your build.gradle file.

dependencies {
implementation 'org.testng:testng:7.7.0'
implementation 'io.appium:java-client:8.1.1'
implementation 'org.seleniumhq.selenium:selenium-java:4.8.1'
}

4. Let’s setup Appium drive on the test class:

  • Set driver capabilities in your code using the same capabilities which we have used on Appium Inspector.

5. In your test class let’s create a @BeforeMethod to setup our driver and @AfterMethod to tear down our driver.

  • You could use other Before/After annotations depends on your needs.
import io.appium.java_client.AppiumBy;
import io.appium.java_client.AppiumDriver;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.android.options.UiAutomator2Options;
import org.openqa.selenium.WebElement;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.net.MalformedURLException;
import java.net.URL;

public class TestYoutube {
public static AppiumDriver driver;

@BeforeMethod
public void setup() throws MalformedURLException {
UiAutomator2Options options = new UiAutomator2Options();
options.setPlatformName("Android");
options.setUdid("emulator-5554");
options.setAppPackage("com.google.android.youtube");
options.setAppActivity("com.google.android.youtube.app.honeycomb.Shell$HomeActivity");
options.setDeviceName("Pixel 4 API 33");
options.setAutomationName("UiAutomator2");
options.setPlatformVersion("13");
options.setAutoGrantPermissions(true);

driver = new AndroidDriver(new URL("http://127.0.0.1:4723"), options);
}

@AfterMethod
public void tearDown() {
driver.quit();
}
}

6. Let’s write our first test.

import io.appium.java_client.AppiumBy;
import io.appium.java_client.AppiumDriver;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.android.options.UiAutomator2Options;
import org.openqa.selenium.WebElement;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.net.MalformedURLException;
import java.net.URL;

public class TestYoutube {
public static AppiumDriver driver;

@BeforeMethod
public void setup() throws MalformedURLException {
UiAutomator2Options options = new UiAutomator2Options();
options.setPlatformName("Android");
options.setUdid("emulator-5554");
options.setAppPackage("com.google.android.youtube");
options.setAppActivity("com.google.android.youtube.app.honeycomb.Shell$HomeActivity");
options.setDeviceName("Pixel 4 API 33");
options.setAutomationName("UiAutomator2");
options.setPlatformVersion("13");
options.setAutoGrantPermissions(true);

driver = new AndroidDriver(new URL("http://127.0.0.1:4723"), options);
}

@AfterMethod
public void tearDown() {
driver.quit();
}

@Test(description = "Test to play my favorite music on Youtube")
public void testPlayMyFavoriteMusicOnYouTube() throws InterruptedException {
// Click on search icon
Thread.sleep(10000);
WebElement searchIcon = driver.findElement(AppiumBy.accessibilityId("Search"));
searchIcon.click();

// Send keys to search bar
Thread.sleep(1000);
WebElement searchBar = driver.findElement(AppiumBy.id("com.google.android.youtube:id/search_edit_text"));
searchBar.sendKeys("Rick Rolled (Short Version)");

// Click on search suggestion
Thread.sleep(1000);
String newXpath = "//android.widget.TextView[@text='rick rolled (short version)']";
WebElement searchSuggestion = driver.findElement(AppiumBy.xpath(newXpath));
searchSuggestion.click();

// Click on search result
Thread.sleep(2000);
String titleXpath = "//android.view.ViewGroup[contains(@content-desc, 'Rick Rolled (Short Version)')]";
WebElement searchResult = driver.findElement(AppiumBy.xpath(titleXpath));
searchResult.click();

// Click on expand video's description
Thread.sleep(2000);
String expandButtonXpath = "//android.view.ViewGroup[@content-desc='Expand description']/android.widget.ImageView";
WebElement expandButton = driver.findElement(AppiumBy.xpath(expandButtonXpath));
expandButton.click();

// Assertion - assert that the uploader name Legacy PNDA is visible
String uploaderNameXpath = "//android.view.ViewGroup[@content-desc='Legacy PNDA']";
WebElement uploaderName = driver.findElement(AppiumBy.xpath(uploaderNameXpath));
Boolean uploaderNameResult = uploaderName.isDisplayed();

Assert.assertTrue(uploaderNameResult, "Verify if uploader name Legacy PNDA is visible");
}
}

Test Execution & Report

  1. Single test using Intellij

2. XML test suites CLI

  • Create XML file under src/test/resources
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >

<suite name="Our First Suite" verbose="1" >
<test name="YouTube test">
<classes>
<class name="TestYoutube"/>
</classes>
</test>
</suite>
  • Add this to your build.gradle, this is used to execute testng.xml from CLI:
def suite1 = project.hasProperty("testng")

test {
useTestNG() {
outputDirectory = file("test-output")
useDefaultListeners = true

if(suite1) {
suites "src/test/resources/testng.xml"
}
}
}
  • Open terminal on your root project and enter following command:
gradle test -Ptestng 
  • After your test finished, a new directory test-output will appear on your root project.
  • Under test-output directory, right click emailable-report.html and open in browser. Then you will be redirected to your browser and the html report will be opened.

Bonus Section

As we use Thread.sleep() in our previous code, it’s actually a bad practice for automation testing. Why? Let’s take a look at this example:

  • Element A need 2 seconds to completely load so then we set the sleep to 2 seconds (time.sleep(2)). But what if the connection is not stable on our next testing and let’s say it might take 5 seconds to completely load, with our default sleep of 2 seconds, then our test will fail.
  • Ok then why don’t we set the sleep to 5 seconds? Good idea, but what if the connection is back to normal and it only take 2 seconds for the app to completely load? Means that our test will be idle for the rest 3 seconds which is a waste of time.
  • What’s the matter with that 3 seconds? It’s just a short of time! In automation testing we could write many lines of code which we will perform many different actions or steps. Imagine if we have to waste 3 seconds foreach steps 😭 This would take more resources of your computer and your test will take much more time to finish, which most of them just being idle 🤣
  • So.. what can we do? Appium comes with the ability to do Explicit Wait which will periodically check for the existence of the element within period of time.
  • Read more about Implicit vs Explicit wait for TestNG here.
  • Let’s refactor our code.
// Before
Thread.sleep(10000);
WebElement searchIcon = driver.findElement(AppiumBy.accessibilityId("Search"));
searchIcon.click();

// After
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement searchIcon = wait.until(ExpectedConditions.visibilityOfElementLocated(AppiumBy.accessibilityId("Search")));
searchIcon.click();

Thank you for learning with me, I hope this article will help you on your first step to become a good SDET. If you have any feedback or questions feel free to contact me thru:

--

--