Clicking application push notifications
Accessing system settings
Navigating from another app to the app being tested and vice versa
The reason for such a limitation lies in the nature of the Android Test instrumentation. Since during an Espresso test run, the application being tested and the test application processes are spawned, we are not allowed to interact with other applications installed on the mobile device, like the notification bar, camera, or system settings applications. But it is possible to access them using the UI Automator. The UI testing framework is suitable for cross-app functional UI testing.
Note
The UI Automator test framework supports Android 4.3 (API level 18) and higher.
A uiautomatorviewer to inspect the layout hierarchy. Starting with Android Studio 2.3, this was replaced by a monitor tool.
An API to retrieve device state information and perform operations on it. The examples are pressing the device home or back button, changing device rotation, opening notifications, and taking a screenshot.
APIs that support cross-application UI testing.
By—By is a utility class that enables the creation of BySelectors in a concise manner. Its primary function is to provide static factory methods for constructing BySelectors using a shortened syntax. For example, you would use findObject(By.text("foo")) rather than findObject(new BySelector().text("foo")) to select UI elements with the text value "foo".
BySelector—A BySelector specifies criteria for matching UI elements during a call to findObject(BySelector).
Configurator—Allows you to set key parameters for running UI Automator tests. The new settings take effect immediately and can be changed any time during the test run.
UiCollection—Used to enumerate a container’s UI elements for the purpose of counting or targeting a subelement by a child’s text or description.
UiObject—A UiObject is a representation of a view. It is not in any way directly bound to a view as an object reference. A UiObject contains information to help it locate a matching view at runtime based on the UiSelector properties specified in its constructor. Once you create an instance of a UiObject, it can be reused for different views that match the selector criteria.
UiObject2—A UiObject2 represents a UI element. Unlike UiObject, it is bound to a particular view instance and can become stale if the underlying view object is destroyed. As a result, it may be necessary to call findObject(BySelector) to obtain a new UiObject2 instance if the UI changes significantly.
UiScrollable—UiScrollable is a UiCollection and provides support for searching for items in scrollable layout elements. This class can be used with horizontally or vertically scrollable controls.
UiSelector—Specifies the elements in the layout hierarchy for tests to target, filtered by properties such as text value, content-description, class name, and state information. You can also target an element by its location in a layout hierarchy.
Until—The Until class provides factory methods for constructing common conditions.
Note
The UI Automator testing framework is an instrumentation-based API and works with the AndroidJUnitRunnertest runner. This fact allows us to use Espresso together with UI Automator code in the same test.
Starting with UI Automator
To start using UI Automator, we should first set the dependency in the build.gradle file , as follows.
And set the AndroidX Test library dependency as well.
- Handling application transitions:
Espresso—Handles window transitions automatically.
UI Automator—Doesn’t support automatic window transitions, i.e., switching between activities or fragments. You should explicitly use waitings.
- Locating UI elements:
Espresso—Core Espresso onView() and onData() methods together with view matchers or data matchers are used to locate the UI element.
UI Automator—Similar to Espresso, UI Automator has its own core UiDevice class. It contains such methods as hasObject(BySelector), findObject(BySelector), and findObjects(BySelector) that are used to search for an element or check its presence in the application UI.
- Waitings:
Espresso—The IdlingResource and third-party ConditionWatcher classes can be used as waiting mechanisms, including both networks related and element presence waitings. Both waiters should be implemented for each specific case.
UI Automator—Unlike Espresso, UI Automator has defined wait() and performActionAndWait() methods in the UiDevice class. Waitings can be easily tweaked by providing custom SearchCondition or EventCondition parameters.
- UI/view actions:
Espresso—Supports a wide range of view actions with the possibility to define your own. It’s able to interact only with the application being tested.
UI Automator—As mentioned, the UI Automator can interact with any application on the device. But the degree of action is very limited.
- Device controls:
Espresso—No support.
UI Automator—Provides a long list of device control methods starting from the device home button click to orientation control.
- Reporting:
Espresso—Supports much richer test failure reports and makes it easy to analyze test failures.
UI Automator—Usually returns nondescriptive stacktraces upon failure, which should be analyzed to realize what went wrong.
Based on these differences, you can see that in combination, these two form a very powerful feature set that covers almost all the needs in the Android test automation.
UiDevice().find—Shows UI Automator find methods.
UiDevice().act—Consolidates all actions that can be done with the device, from pressing the back device button to a shell command execution.
UiDevice().wait—Waits for a certain condition to be fulfilled.
UiDevice().watch—Represents a group of methods used to create and control condition watchers.
UiDevice().get—Retrieves device or application parameters.
UiDevice().set—Enables or disables layout hierarchy compression.
In the following sections, we will see how most of these UiDevice methods can be used when Espresso for Android cannot handle the issue.
Finding and Acting on UI Elements
The main UI Automator functionality is locating UI elements and performing actions on them. If we talk about UI Automator usage in combination with Espresso, then it can be used to navigate through third-party or system applications, take screenshots or, for example, execute shell commands. But, of course, the UI Automator can act as a standalone test automation framework.
UiSelector()
BySelector()
The conceptual difference between them is in the way that search criteria specified by each selector is applied. To understand this, we will first look at the UiSelector sample tests implemented in the UiAutomatorUiSelectorTest.kt test class.
As you can see, the UI Automator test contains an ActivityTestRule rule to start the application main TasksActivity . After that, the UI Automator code takes over the test execution and performs the UI interactions. All the resourceId values were taken from the monitor tool after dumping the application layout.
Also notice that the test code is barely readable because of the uiDevice.findObject(UiSelector) method calls on each test step. But there is a simple fix⎯since uiDevice.findObject(UiSelector) returns an UiObject instance that locates a matching view at runtime, it can be declared in advance and later reused for different views that match the selection criteria.
This is how the test method will look after simplification.
This test method looks much nicer and is more readable. As a side effect, you receive easily maintainable code, so whenever element properties like id or class are changed, it will be enough to update them once in the declaration instead of changing them across the test code.
Let’s move forward to the BySelector test samples implemented in the UiAutomatorBySelectorTest.kt class. The bySelectorSample() test demonstrates how the same test scenario automated in the UiAutomatorUiSelectorTest.kt class can be tested using BySelector and UiObject2.
The test code with BySelector may initially look more readable, but there is one drawback—BySelector() is applied and executed during a call to UiDevice.findObject(BySelector) , reducing the flexibility in test code writing. That means the following line cannot be declared as a variable at the beginning of the test method and later reused.
What we still can do is extract the selector itself, as follows.
The possible test method improvements are shown in the following code.
The last method also has a findObjects(BySelector) sample . In our specific case, we use this method to get the list of TO-DO items and then navigate through its items based on the position in the list.
This should be it about finding and acting on elements using the UI Automator. Of course, we haven’t covered all the possible search criteria and actions, but the examples we discuss should be a good basis for you to move forward.
Waiting for UI Elements
In such test frameworks like UI Automator—where automated tests interact with multiple applications and where we don’t have much control over network requests execution, application transitions, or animations—it is important to have proper waiting mechanisms that will allow us to write more reliable test code.
Waiting for EventCondition—A condition that depends on an event or series of events having occurred.
Waiting for SearchCondition—A condition that is satisfied by searching for UI elements.
Waiting for UiObject2Condition—A condition that is satisfied when a UiObject2 is in a particular state.
All the conditions are implemented in the android.support.test.uiautomator.Until.java class in the Android Testing Support library or in androidx.test.uiautomator inside the AndroidX Test library.
The previous section contains some waiting examples and you probably noticed them. Waiting for an EventCondition was used in the following line and is responsible for finding the Add Task floating action button, clicking it, and then waiting for a new window to be presented to the user.
newWindow()—Returns a condition that depends on a new window having appeared.
scrollFinished()—Returns a condition that depends on a scroll having reached the end in the given direction.
gone()—Returns a link SearchCondition that is satisfied when no elements matching the selector can be found.
hasObject()—Returns a link SearchCondition that is satisfied when at least one element matching the selector can be found.
findObject()—Returns a SearchCondition that is satisfied when at least one element matching the selector can be found. The condition will return the first matching element.
findObjects()—Returns a link SearchCondition that is satisfied when at least one element matching the selector can be found. The condition will return all matching elements.
checkable()—Returns a condition that depends on a UiObject2 checkable state.
checked()—Returns a condition that depends on a UiObject2 checked state.
clickable()—Returns a condition that depends on a UiObject2 clickable state.
enabled()—Returns a condition that depends on a link UiObject2 enabled state.
focusable()—Returns a condition that depends on a link UiObject2 focusable state.
focused()—Returns a condition that depends on a UiObject2’s focused state.
longClickable()—Returns a condition that depends on a UiObject2’s long clickable state.
scrollable()—Returns a condition that depends on a UiObject2’s scrollable state.
selected()—Returns a condition that depends on a UiObject2’s selected state.
descMatches()—Returns a condition that is satisfied when the object’s content description matches the given regex.
descEquals()—Returns a condition that is satisfied when the object’s content description exactly matches the given string.
descContains()—Returns a condition that is satisfied when the object’s content description contains the given string.
descStartsWith()—Returns a condition that is satisfied when the object’s content description starts with the given string.
descEndsWith()—Returns a condition that is satisfied when the object’s content description ends with the given string.
textMatches()—Returns a condition that is satisfied when the object’s text value matches the given regex.
textNotEquals()—Returns a condition that is satisfied when the object’s text value does not match the given string.
textEquals()—Returns a condition that is satisfied when the object’s text value exactly matches the given string.
textContains()—Returns a condition that is satisfied when the object’s text value contains the given string.
textStartsWith()—Returns a condition that is satisfied when the object’s text value starts with the given string .
textEndsWith()—Returns a condition that is satisfied when the object’s text value ends with the given string.
The list is big enough and covers most used elements properties. They are similar to Espresso’s ViewMatchers, which we are already familiar with.
Waiting for UiObject2Condition can be demonstrated by the following line of code, which searches for the first element inside the TO-DO recycler view list, locates the checkbox element in it, and waits until it is checked.
Considering what we’ve covered so far, we can admit that UI Automator is a powerful test framework that can be used as a standalone test automation tool. But wait, we haven’t yet unleashed its full power. Let’s move to the next section and see what it prepared for us.
Watching for Conditions
checkForCondition()⎯Custom handler that is automatically called when the testing framework is unable to find a match using the UiSelector.
The checkForCondition() method is called automatically when UI Automator framework is in the process of matching a UiSelector and it is unable to match any element based on the specified criteria in the selector. When this happens, the callback will perform retries for a predetermined time, waiting for the display to update and show the desired widget. While the framework is in this state, it will call registered watchers’ checkForCondition(). This gives the registered watchers a chance to look at the display and see if there is a recognized condition that can be handled. In doing so, this allows the current test to continue.
The possible use cases where UiWatcher can be useful can be handling one-time popups like low battery level dialogs, application feedback dialogs, advertisements, and permission granting for third-party applications. The beauty of this approach is that UiWatcher should not be part of the test method but can be registered once per test class or per test package and act only when there is a need.
registerWatcher()—Registers a UiWatcher to run automatically when the testing framework is unable to find a match using a UiSelector.
removeWatcher()—Removes a previously registered UiWatcher.
resetWatcherTriggers()—Resets a UiWatcher that has been triggered. If a UiWatcher runs and its checkForCondition() call returns true, then the UiWatcher is considered triggered.
runWatchers()—This method forces all registered watchers to run.
As an example, the TO-DO application’s Statistics screen shows a dialog that must be dismissed. Open the UiAutomatorUiWatcherTest.kt class to see the details.
If we break it down, we will see that the UiWatcher instance is created first.
Then, from the setUp() method that will be executed before each test run, we call registerStatisticsDialogWatcher() to register the watcher and run it.
At this point, everything is ready for running the dismissesStatisticsDialogUsingWatcher() test . The test starts the application, opens the menu drawer, and navigates to the Statistics section, where the AlertDialog is popping up. Then the UI Automator framework tries to locate the Statistics text but can’t. The UiWatcher mechanism starts to check if there is something on the screen that was expected to be cached by the running watcher. In our case, it is the AlertDialog OK button, which is clicked from inside the watcher.
In general, it is worth trying to use UiWatcher in automated tests, which can enrich test automation tooling and make test more legible.
Combining Espresso and UI Automator in Tests
At this point it should be clear enough how to use the UI Automator test framework as a standalone test automation tool and it is a time to reveal the full power of Android test automation using both Espresso and UI Automator inside a single test. To demonstrate this, we will automate the use case where the TO-DO application sends the notification, which, after clicked, opens TasksActivity (i.e., tasks list screen) to the user. The first part of the test is automated using Espresso, starting from the moment when we click Notification, the UI Automator will be used. At the end, the Espresso code will be used again to verify the state of the application.
Let’s take a look at the test itself.
If the notification is delayed, we can wait for it using the wait() method. This case is covered by the second test method in the same class. The TO-DO application sends a notification with a small delay and the test handles it by waiting for the notification object.
As you can see, with both frameworks, we can cover most of the use cases we need, starting from testing an application using Espresso to more complicated cases like interacting with notifications, opening system settings, and dealing with other third-party applications using UI Automator.
Exercise 20
- 1.
Implement a test using the UI Automator UiSelector that creates and then modifies the TO-DO item.
- 2.
Implement a test using the UI Automator BySelector that creates two TO-DO items, marks one as done, filters out the active TO-DO item, and verifies it.
- 3.
Implement a test that opens a contextual menu in the TO-DO list toolbar and clicks on the share button. Modify the existing UiWatcher to wait for Gmail application icon/text shown in the application chooser and click on it from inside the UiWatcher.
- 4.
Implement a test that uses Espresso and the UI Automator code and automates the process described in Step 3.
Summary
Depending on its goal, an Android UI test may target different applications: instrumented applications, third-party applications, or both. In the case of third-party or mixed applications, the testing is performed using the UI Automator framework, which is a powerful testing tool that allows wider test coverage compared to pure Espresso tests. Combining Espresso and the UI Automator framework creates UI tests that are powerful enough to cover most of the use cases we can imagine.