Blog Infos
Author
Published
Topics
, , ,
Author
Published
Posted by: Heitor Paceli

Automation is a key point of Software Testing once it make possible to reproduce test steps as many times as needed, across the different software versions, which can be tedious to be done manually and very error prone once it is likely to a human to forget to perform some required set up or misunderstand some test step, resulting in invalid results.

This article will explain how to write an UI Automator script to automatically test any Android application. We will write a simple test for the Android Settings app that adds a Wi-Fi network and check if the device is able to connect to it.

The example from this article was written using an Android Virtual Device with the system image R API level 30 ABI x86 target Android 11 (Google Play). You can use any image with Android 4.3 (API level 18) or higher and adapt the code according to its UI.

UI Automator

UI Automator is an Android testing framework that allows us to write scripts that can interact to any application installed in the device. UI Automator doesn’t require having access to the application source code to work. Because of that, the script can navigate and interact with the application tray, Settings application, third party applications or any other app you want to.

Creating the project

Create a new Project on Android Studio with no activity and minimum SDK 18 or higher. The Android Studio will create three different source sets in the project: mainandroidTest and test. This is the default project structure in which main contains the application code, the test contains the unit tests that runs on the development machine and androidTest that is were the instrumented tests, like UI Automator ones, go by default.

If you are writing you scripts in the same project of your application, then you can just create your scripts inside androidTest directory, but in this article we are creating a project with UI Automator scripts only, to test another application which we don’t have access to its source code, so we will make some changes before starting it.

Open the build.gradle of the app module and add the following code inside android block:

sourceSets {
androidTest {
java.srcDir 'src/main/java'
}
}
view raw build.gradle hosted with ❤ by GitHub

This will change the directory of our instrumented tests to the maindirectory. Now you can delete both test and androidTest source sets. Also delete some resources that will no longer be used:

app/src/main/res/values/themes.xml
app/src/main/res/values-night/themes.xml
app/src/main/res/values/colors.xml

With our source sets configured, let’s add the required dependencies. Once our scripts are part of the main source set, we need to add the dependencies using implementation instead of androidTestImplementation. Also you can remove dependencies that will not be used:

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.test.ext:junit:1.1.3'
implementation 'androidx.test:runner:1.4.0'
implementation 'androidx.test.uiautomator:uiautomator:2.2.0'
}
view raw build.gradle hosted with ❤ by GitHub

After the change this is how your build.gradle will look like:

plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.paceli.wifitest"
minSdkVersion 18
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
androidTest {
java.srcDir 'src/main/java'
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.test.ext:junit:1.1.3'
implementation 'androidx.test:runner:1.4.0'
implementation 'androidx.test.uiautomator:uiautomator:2.2.0'
}
view raw build.gradle hosted with ❤ by GitHub

We are almost done configuring the project, we just need to modify the AndroidManifest.xml in order to remove reference to the deleted files and add the instrumentation. Open the manifest file and remove the themeatribute from application tag once we deleted theme XML files previously.

Once we are creating the scripts in the main source set, we need to manually add the instrumentation tag to the manifest files. Add the instrumentation inside the manifest tag with the name and targetPackageattributes. The name must be defined as androidx.test.runner.AndroidJUnitRunner and the targetPackage must be the same package of your application (which is com.paceli.wifitest, in this example).

You can see the AndroidManifest.xml with the changes below:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.paceli.wifitest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" />
<instrumentation
android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.paceli.wifitest" />
</manifest>
Writing our script

Having created and configured the project, we are good to proceed creating our first script. Create a new class and add the annotation @RunWith(AndroidJUnit4::class) in order to define AndroidJUnit4 as the test runner.

As usual in JUnit, the test methods must be annotated with @Test. Set up and tear down methods must be annotated with @Before and @Afterrespectively. For those who are not familiar with this approach, the code below shows the order that the methods are executed:

package com.paceli.wifitest
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.*
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class UiAutomatorOrder {
/**
* Run before the method with @Test annotation
*/
@Before
fun before() {
Log.d(TAG, "Before")
}
/**
* Run after the method with @Before annotation
* and before methods with @After annotation
*/
@Test
fun test() {
Log.d(TAG, "Test")
}
/**
* Run after each method with @Test annotation
*/
@After
fun after() {
Log.d(TAG, "After")
}
companion object {
private const val TAG = "UiAutomatorExample"
/**
* Run once before other methods from [UiAutomatorOrder] class
*/
@JvmStatic
@BeforeClass
fun beforeClass() {
Log.d(TAG, "Before Class")
}
/**
* Run once after other methods from [UiAutomatorOrder] class
*/
@JvmStatic
@AfterClass
fun afterClass() {
Log.d(TAG, "After Class")
}
}
}

Running this example will produce the following logcat output:

Before Class
Before
Test
After
After Class

So let’s create a method named validateWifi in out test class and annotate it with @Test. In order to click on buttons, read text from screen, perform swipe gestures, and any other interaction with the UI, we need to get an instance of the UiDevice class. To achieve that, we declared a property to the class and added the init block with the code to get an instance. This is how our class is looking at this moment:

package com.paceli.wifitest
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class WifiTest {
private val device: UiDevice
init {
val instrumentation = InstrumentationRegistry.getInstrumentation()
device = UiDevice.getInstance(instrumentation)
}
@Test
fun validateWifi() {
}
}
view raw WifiTest.kt hosted with ❤ by GitHub

To interact with the elements from the screen, we need to get a reference to them using the UiDevice instance. To do that, we will call the method findObject passing the properties of the element we want to interact with. Some properties, like the text, are visible to the user, but there are several others that we cannot see on the device screen, so we need to dump the screen using the UI Automator Viewer tool that is available in the Android SDK at $ANDROID_HOME/tools/bin/uiautomatorviewer.

By clicking on the Device Screenshot button on the top left, the tool will show a screenshot and the dump of the screen currently displayed from the device connected to ADB. The elements can be chosen by clicking on them at the screenshot or at hierarchy view on the right side. Properties like indextextcontent-descenabled, etc. are displayed in the Node detailview and.

The UiObject2 class represents the elements from screen and an instance of it is returned by the findObject method. In order to launch the Settings app, the user need to perform a scroll gesture on home screen to launch the applications list, and then click on Settings icon. So we obtained an instance of UiObject2 representing the workspace and another one for the Settings icon in the apps list screen, and then call the methods scroll and click respectively.

Job Offers

Job Offers


    Lead Android Engineer

    ASOS
    London
    • Full Time
    apply now

    Developer (m/w/d) Backend/ Mobile

    Payback GmbH
    Cologne, Germany
    • Full Time
    apply now

    Senior Android Engineer – Big Release Team

    Zalando SE
    Berlin
    • Full Time
    apply now
Load more listings

tired of reading

Take a break an watch a video

No results found.

@Test
fun validateWifi() {
// Open apps list by scrolling on home screen
val workspace = device.findObject(
By.res("com.google.android.apps.nexuslauncher:id/workspace")
)
workspace.scroll(Direction.DOWN, 1.0f)
// Click on Settings icon to launch the app
val settings = device.findObject(
By.res("com.google.android.apps.nexuslauncher:id/icon").text("Settings")
)
settings.click()
}
view raw WifiTest.kt hosted with ❤ by GitHub

If some element takes some time to be displayed in the screen, we can use the method wait instead of the findObject. This method receives a SearchCondition and a timeout. Let’s use this approach to open the Network & internet section and then proceed up to the Add network screen.

// ...
// Wait up to 2 seconds for the element be displayed on screen
val networkAndInternet = device.wait(Until.findObject(By.text("Network & internet")), 2000)
networkAndInternet.click()
// Click on element with text "Wi‑Fi"
val wifi = device.wait(Until.findObject(By.text("Wi‑Fi")), 2000)
wifi.click()
// Click on element with text "Add network"
val addNetwork = device.wait(Until.findObject(By.text("Add network")), 2000)
addNetwork.click()
view raw WifiTest.kt hosted with ❤ by GitHub

In the Add network screen, there is a text field where the user must input the network SSID. To input text in a UI Automator script we just need to obtain the UiObject2 instance of this field and call the setText passing the string we want to input.

We will input the default name of Wi-Fi network in Android Virtual Device AndroidWifi (update accordingly to your needs) and then click on Savebutton to add it.

// ...
// Obtain an instance of UiObject2 of the text field
val ssidField = device.wait(Until.findObject(By.res("com.android.settings:id/ssid")), 2000)
// Call the setText method using Kotlin's property access syntax
val ssid = "AndroidWifi"
ssidField.text = ssid
//Click on Save button
device.findObject(By.res("android:id/button1").text("Save")).click()
view raw WifiTest.kt hosted with ❤ by GitHub

If everything went right, now the added Wi-Fi network must be listed in the the list of networks and the Android device must be connected to it (for simplicity’s sake, we are assuming there is no other saved Wi-Fi network before running this test).

In order to check if the Wi-Fi was correctly added and the Android device is connected to it, we can simply check if the word Connected is displayed below the network we just added. To do that let’s use the method hasObject that returns a boolean indicating whether some element is currently being displayed on screen.

// ...
// BySelector matching the just added Wi-Fi
val ssidSelector = By.text(ssid).res("android:id/title")
// BySelector matching the connected status
val status = By.text("Connected").res("android:id/summary")
// BySelector matching on entry of Wi-Fi list with the desired SSID and status
val networkEntrySelector = By.clazz(RelativeLayout::class.qualifiedName)
.hasChild(ssidSelector)
.hasChild(status)
// Perform the validation using hasObject
// Wait up to 5 seconds to find the element we're looking for
val isConnected = device.wait(Until.hasObject(networkEntrySelector), 5000)
Assert.assertTrue("Verify if device is connected to added Wi-Fi", isConnected)
view raw WifiTest.kt hosted with ❤ by GitHub

We also have the option to use the Android APIs in order to get the SSID of current Wi-Fi the device is connected to. This is possible because the UI Automator script is installed as an Android application, so it has access to the APIs commonly used during applications development like intents, system services, contexts, etc.

To be able to get the Wi-Fi SSID, add these permissions to the AndroidManifest.xml:

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

The following method get the Context of the application (UI Automator script) and use it to get an instance of WifiManager and obtain the Wi-Fi SSID. We will use the return value to compare to the name of the network we added before.

private fun getCurrentWifiSsid(): String? {
val context = InstrumentationRegistry.getInstrumentation().context
val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
val wifiInfo = wifiManager.connectionInfo
// The SSID is quoted, then we need to remove quotes
return wifiInfo.ssid?.removeSurrounding("\"")
}
view raw WifiTest.kt hosted with ❤ by GitHub

Now the test method is ready. It will look like this:

@Test
fun validateWifi() {
// Open apps list by scrolling on home screen
val workspace = device.findObject(
By.res("com.google.android.apps.nexuslauncher:id/workspace")
)
workspace.scroll(Direction.DOWN, 1.0f)
// Click on Settings icon to launch the app
val settings = device.findObject(
By.res("com.google.android.apps.nexuslauncher:id/icon").text("Settings")
)
settings.click()
// Wait up to 2 seconds for the element be displayed on screen
val networkAndInternet = device.wait(Until.findObject(By.text("Network & internet")), 2000)
networkAndInternet.click()
// Click on element with text "Wi‑Fi"
val wifi = device.wait(Until.findObject(By.text("Wi‑Fi")), 2000)
wifi.click()
// Click on element with text "Add network"
val addNetwork = device.wait(Until.findObject(By.text("Add network")), 2000)
addNetwork.click()
// Obtain an instance of UiObject2 of the text field
val ssidField = device.wait(Until.findObject(By.res("com.android.settings:id/ssid")), 2000)
// Call the setText method using Kotlin's property access syntax
val ssid = "AndroidWifi"
ssidField.text = ssid
//Click on Save button
device.findObject(By.res("android:id/button1").text("Save")).click()
// BySelector matching the just added Wi-Fi
val ssidSelector = By.text(ssid).res("android:id/title")
// BySelector matching the connected status
val status = By.text("Connected").res("android:id/summary")
// BySelector matching on entry of Wi-Fi list with the desired SSID and status
val networkEntrySelector = By.clazz(RelativeLayout::class.qualifiedName)
.hasChild(ssidSelector)
.hasChild(status)
// Perform the validation using hasObject
// Wait up to 5 seconds to find the element we're looking for
val isConnected = device.wait(Until.hasObject(networkEntrySelector), 5000)
Assert.assertTrue("Verify if device is connected to added Wi-Fi", isConnected)
// Perform the validation using Android APIs
val connectedWifi = getCurrentWifiSsid()
Assert.assertEquals("Verify if is connected to the Wifi", ssid, connectedWifi)
}
view raw WifiTest.kt hosted with ❤ by GitHub

To have a complete test script, we need to perform some set up before the actual test is executed. If you tried running the script while the screen was other than the home screen, you noticed that a NullPointerExceptionoccurs. That happens because we assumed the device was in the home screen when the test starts. In order to guarantee that, add the following set up method with the @Before annotation.

@Before
fun setUp() {
// Press Home key before running the test
device.pressHome()
}
view raw WifiTest.kt hosted with ❤ by GitHub

Now the Home key will always be pressed before running the test. You may want to return to home screen after after the test execution as well, to do that simply add another method with the @After annotation similar to what we did with the set up method.

@After
fun tearDown() {
// Press Home key after running the test
device.pressHome()
}
view raw WifiTest.kt hosted with ❤ by GitHub

In summary set up methods are used to make sure that the device is in an required initial state. Tear down methods are responsible to perform a clean up after running the test in order to not influence the subsequent ones. Let’s suppose you are writing a script that adds a password to the device. This password must be removed on the tear down method, otherwise next scripts may be stuck in password screen.

Running the script using ADB

Once you created the scripts you probably may want to execute them outside Android Studio. To do that you need to build .apk file, install it on Android device and execute the test using ADB.

To build the .apk you can simply go to Build -> Make Project on Android Studio, or run the Gradle command:

# On Linux
./gradlew assembleDebug# On Windows
gradlew.bat assembleDebug

The .apk with the scripts will be available at app/build/outputs/apk/debug(app is the module’s name). With the .apk built, install it like any other application with the command:

adb install -r -g app-debug.apk

Finally, start the execution by running the following command:

adb shell am instrument -w -e class 'com.paceli.wifitest.WifiTest' com.paceli.wifitest/androidx.test.runner.AndroidJUnitRunner

com.paceli.wifitest.WifiTest — is the class name of the test script to be executed.
com.paceli.wifitest/androidx.test.runner.AndroidJUnitRunner — is the test package name and the runner class in the format <test_package_name>/<runner_class> .

See more details in the official documentation.

Conclusion

This is is how we create a UI Automator test script with no access to the application source code. I hope this article may have helped you creating your own test scripts, improving test coverage for you application and reducing manual effort.

The full project with the code presented in this article is available in this GitHub repository. Fell free to clone and modify it according to your needs. A good exercise may be to modify the code in order to launch the Wi-Fi activity directly, using intents, reducing the execution time, or to modify the set up ensuring that there isn’t any saved network before running the test. It’s up to you.

If you find this article helpful, please don’t forget to clap. I would also appreciate your feedback in the comments section. Thanks!

 

heitorpaceli/ui-automator-guide

You can’t perform that action at this time. You signed in with another tab or window. You signed out in another tab or…

github.com

 

Tags: Android, Testing, Test Automation, Automation, Kotlin

View original article at:


Originally published: July 10, 2021

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu