© Denys Zelenchuk 2019
Denys ZelenchukAndroid Espresso Revealedhttps://doi.org/10.1007/978-1-4842-4315-2_8

8. Espresso and UI Automator: the Perfect Tandem

Denys Zelenchuk1 
(1)
Zürich, Switzerland
 
Espresso is a perfect and fast test automation framework, but it has one important limitation—we are allowed to operate only inside our test application context. This means that it is not possible to automate tests for the following use cases:
  • 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.

The key features of the UI Automator include the following:
  • 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.

We will focus a bit on the UI Automator APIs:
  • 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.

  • UiScrollableUiScrollable 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.

In addition to this list of APIs, we should be familiar with the following framework class:
  • 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.

UI Automator Android Testing Support Library Dependency in the build.gradle File.
    androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'

And set the AndroidX Test library dependency as well.

UI Automator AndroidX Test Library Dependency in the build.gradle File.
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
Let’s analyze the UI Automator from an Espresso perspective, which we are already familiar with, and try to figure out how different they are. We will also consider the UI Automator’s strengths and weakness:
  • 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.

To more easily understand the UI Automator framework, we can describe its features using verbs:
  • UiDevice().find—Shows UI Automator find methods.

../images/469090_1_En_8_Chapter/469090_1_En_8_Figa_HTML.jpg
  • UiDevice().act—Consolidates all actions that can be done with the device, from pressing the back device button to a shell command execution.

../images/469090_1_En_8_Chapter/469090_1_En_8_Figb_HTML.jpg
  • UiDevice().wait—Waits for a certain condition to be fulfilled.

../images/469090_1_En_8_Chapter/469090_1_En_8_Figc_HTML.jpg
  • UiDevice().watch—Represents a group of methods used to create and control condition watchers.

../images/469090_1_En_8_Chapter/469090_1_En_8_Figd_HTML.jpg
  • UiDevice().get—Retrieves device or application parameters.

../images/469090_1_En_8_Chapter/469090_1_En_8_Fige_HTML.jpg
  • UiDevice().set—Enables or disables layout hierarchy compression.

../images/469090_1_En_8_Chapter/469090_1_En_8_Figf_HTML.jpg

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.

As was described, the UI element search is done by the findObject() and findObjects() methods . The findObject() method takes instances of the following classes as parameters that specify criteria for matching UI elements in the hierarchy:
  • 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.

chapter8 .UiAutomatorUiSelectorTest.uiSelectorSample().
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
private val fourSecondsTimeout = 4000L
@get:Rule
var activityTestRule = ActivityTestRule(TasksActivity::class.java)
/**
 * Creates two TO-DO items, marks first as done and verifies its text.
 */
@Test
fun uiSelectorSample() {
    // Add first To-Do item.
    uiDevice.findObject(
            UiSelector().resourceId(
                    "com.example.android.architecture.blueprints.todoapp.mock:id/fab_add_task"))
            .click()
    uiDevice.findObject(UiSelector().resourceId(
            "com.example.android.architecture.blueprints.todoapp.mock:id/add_task_title"))
            .text = "item 1"
    uiDevice.findObject(UiSelector().resourceId(
            "com.example.android.architecture.blueprints.todoapp.mock:id/fab_edit_task_done"))
            .click()
    uiDevice.findObject(UiSelector().text("TO-DO saved")).waitUntilGone(fourSecondsTimeout)
    // Add second To-Do item.
    uiDevice.findObject(UiSelector().resourceId(
            "com.example.android.architecture.blueprints.todoapp.mock:id/fab_add_task"))
            .click()
    uiDevice.findObject(UiSelector().resourceId(
            "com.example.android.architecture.blueprints.todoapp.mock:id/add_task_title"))
            .text = "item 2"
    uiDevice.findObject(UiSelector().resourceId(
            "com.example.android.architecture.blueprints.todoapp.mock:id/fab_edit_task_done"))
            .click()
    uiDevice.findObject(UiSelector().text("TO-DO saved")).waitUntilGone(fourSecondsTimeout)
    // Mark first To-Do item as done, click on it and validate text.
    uiDevice.findObject(UiSelector().className(RecyclerView::class.java.name)
            .childSelector(UiSelector().checkable(true)))
            .click()
    uiDevice.findObject(UiSelector().className(RecyclerView::class.java.name)
            .childSelector(UiSelector().className(LinearLayout::class.java)).instance(0))
            .click()
    val detailViewTitle = uiDevice.findObject(UiSelector().resourceId(
            "com.example.android.architecture.blueprints.todoapp.mock:id/task_detail_title"))
    assertTrue("To-Do "item 1" is not shown.", detailViewTitle.exists())
    assertTrue("To-Do "item 1" is not shown.", detailViewTitle.text.equals("item 1"))
}

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.

chapter8 . UiAutomatorUiSelectorTest.uiSelectorSampleSimplified() .
    /**
     * Shows how interactsWithToDoInRecyclerViewUiSelector() test can be simplified
     * by declaring UiObject elements in advance.
     */
    @Test
    fun uiSelectorSampleSimplified() {
        // Declare UiObject instances that will be used later in test.
        val fabAddTask = uiDevice.findObject(UiSelector().resourceId(
                "com.example.android.architecture.blueprints.todoapp.mock:id/fab_add_task"))
        val taskTitle = uiDevice.findObject(UiSelector().resourceId(
                "com.example.android.architecture.blueprints.todoapp.mock:id/add_task_title"))
        val fabDone = uiDevice.findObject(UiSelector().resourceId(
                "com.example.android.architecture.blueprints.todoapp.mock:id/fab_edit_task_done"))
        val todoSavedText = uiDevice.findObject(UiSelector().text("TO-DO saved"))
        val taskDetailsTitle = uiDevice.findObject(UiSelector().resourceId(
                "com.example.android.architecture.blueprints.todoapp.mock:id/task_detail_title"))
        val firstTodoCheckbox = uiDevice.findObject(UiSelector()
                .className(RecyclerView::class.java.name)
                .childSelector(UiSelector().checkable(true)).instance(0))
        val firstTodoItem = uiDevice.findObject(UiSelector().className(RecyclerView::class.java.name)
                .childSelector(UiSelector().className(LinearLayout::class.java)).instance(0))
        // Add first To-Do item.
        fabAddTask.click()
        taskTitle.text = "item 1"
        fabDone.click()
        todoSavedText.waitUntilGone(fourSecondsTimeout)
        // Add second To-Do item.
        fabAddTask.click()
        taskTitle.text = "item 2"
        fabDone.click()
        todoSavedText.waitUntilGone(fourSecondsTimeout)
        // Mark first To-Do item as done, click on it and validate text.
        firstTodoCheckbox.click()
        firstTodoItem.click()
        assertTrue("To-Do "item 1" is not shown.", taskDetailsTitle.exists())
        assertTrue("To-Do "item 1" title was wrong.", taskDetailsTitle.text.equals("item 1"))
    }

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.

chapter8 .UiAutomatorBySelectorTest.bySelectorSample().
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
private val twoSeconds = 2000L
private val fourSeconds = 4000L
private val applicationPackage = "com.example.android.architecture.blueprints.todoapp.mock"
@get:Rule
var activityTestRule = ActivityTestRule(TasksActivity::class.java)
/**
 * Creates two To-Do items, marks first as done and verifies its text.
 */
@Test
fun bySelectorSample() {
    // Add first To-Do item.
    uiDevice.wait(
            Until.findObject(By.res(applicationPackage, "fab_add_task")), twoSeconds)
            .clickAndWait(Until.newWindow(), twoSeconds)
    uiDevice.findObject(By.res(applicationPackage, "add_task_title")).text = "item 1"
    uiDevice.findObject(By.res(applicationPackage, "fab_edit_task_done"))
            .clickAndWait(Until.newWindow(), twoSeconds)
    uiDevice.wait(Until.gone(By.text("TO-DO saved")), fourSeconds)
    // Add second To-Do item.
    uiDevice.wait(Until.findObject(By.res(applicationPackage, "fab_add_task")), twoSeconds)
            .clickAndWait(Until.newWindow(), twoSeconds)
    uiDevice.findObject(By.res(applicationPackage, "add_task_title")).text = "item 2"
    uiDevice.findObject(By.res(applicationPackage, "fab_edit_task_done"))
            .clickAndWait(Until.newWindow(), twoSeconds)
    uiDevice.wait(Until.gone(By.text("TO-DO saved")), fourSeconds)
    // Mark first To-Do item as done, click on it and validate text.
    val todoList = uiDevice.findObject(By.clazz(RecyclerView::class.java))
    todoList.children[0]
            .findObject(By.checkable(true))
            .click()
    todoList.children[0]
            .click()
    assertTrue("To-Do "item 1" is not shown.", uiDevice.hasObject(By.text("item 1")))
}

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.

chapter8 .UiAutomatorBySelectorTest.bySelectorSample(): Clicking Add Task Floating Action Button.
uiDevice.findObject(By.res(applicationPackage, "fab_edit_task_done")).click()

What we still can do is extract the selector itself, as follows.

chapter8 .UiAutomatorBySelectorTest.bySelectorSample(): Finding and Clicking Done Floating Action Button.
val fabDone = By.res(applicationPackage, "fab_edit_task_done")
uiDevice.findObject(fabDone).click()

The possible test method improvements are shown in the following code.

chapter8 . UiAutomatorBySelectorTest.bySelectorSampleWithFindObjects() .
    @Test
    fun bySelectorSampleWithFindObjects() {
        val fabAddTask = By.res(applicationPackage, "fab_add_task")
        val taskTitle = By.res(applicationPackage, "add_task_title")
        val fabDone = By.res(applicationPackage, "fab_edit_task_done")
        val todoSavedText = By.text("TO-DO saved")
        val checkBox = By.checkable(true)
        val toDoRecyclerView = By.clazz(RecyclerView::class.java)
        // Add first To-Do item.
        uiDevice.waitForWindowUpdate(uiDevice.currentPackageName, twoSeconds)
        uiDevice.wait(Until.findObject(fabAddTask), twoSeconds)
                .clickAndWait(Until.newWindow(), twoSeconds)
        uiDevice.findObject(taskTitle).text = "item 1"
        uiDevice.findObject(fabDone)
                .clickAndWait(Until.newWindow(), twoSeconds)
        uiDevice.wait(Until.gone(todoSavedText), fourSeconds)
        // Add second To-Do item.
        uiDevice.wait(Until.findObject(fabAddTask), twoSeconds)
                .clickAndWait(Until.newWindow(), twoSeconds)
        uiDevice.findObject(taskTitle).text = "item 2"
        uiDevice.findObject(fabDone)
                .clickAndWait(Until.newWindow(), twoSeconds)
        uiDevice.wait(Until.gone(todoSavedText), fourSeconds)
        // Mark first To-Do item as done, click on it and validate text.
        // Showcases findObjects() method use.
        val todoListItems = uiDevice.findObjects(toDoRecyclerView)
        todoListItems[0].findObject(checkBox).click()
        todoListItems[0].click()
        assertTrue("To-Do "item 1" is not shown.", uiDevice.hasObject(By.text("item 1")))
    }

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.

UI Automator waitings are presented by three types:
  • 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.

chapter8 .UiAutomatorUiWatcherTest.kt: Instantiating UiWatcher Object.
uiDevice.wait(
            Until.findObject(By.res(applicationPackage, "fab_add_task")), twoSeconds)
            .clickAndWait(Until.newWindow(), twoSeconds)
Here, EventCondition is a change from the current window to a new one. It is used only as a parameter to the clickAndWait(EventCondition, Timeout) method. Here is are the EventConditions:
  • 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.

SearchCondition is responsible for locating elements in the layout and represents the second waitings group:
  • 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.

We already used the gone() method while waiting for snackbar with the TO-DO saved text gone in the create TO-DO flow, as shown here:
        uiDevice.wait(Until.gone(By.text("TO-DO saved"), twoSeconds)
Here is an example of hasObject() :
        uiDevice.wait(Until.hasObject(By.text("TO-DO saved"), twoSeconds)
And the last type is UiObject2Condition . It waits for the specific UI object state or property:
  • 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.

Waiting for UiObject2Condition Sample.
uiDevice.findObject(By.clazz(RecyclerView::class.java)).children[0]
        .findObject(By.clickable(true))
        .wait(Until.checked(true), twoSeconds)

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

There is one not widely known UI Automator feature that can add big value to your automated tests. The UiWatcher class represents a conditional watcher on the target device being tested. It contains only one method:
  • 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.

In order to control the UiWatcher states , there is list of methods in the UiDevice class:
  • 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.

chapter8 .UiAutomatorUiWatcherTest.kt.
@RunWith(AndroidJUnit4::class)
class UiAutomatorUiWatcherTest {
    @get:Rule
    var activityTestRule = ActivityTestRule(TasksActivity::class.java)
    @Before
    // Register dialog watcher.
    fun before() = registerStatisticsDialogWatcher()
    @After
    fun after() = uiDevice.removeWatcher("StatisticsDialog")
    @Test
    fun dismissesStatisticsDialogUsingWatcher() {
        val toolbar =
                "com.example.android.architecture.blueprints.todoapp.mock:id/toolbar"
        val menuDrawer =
                "com.example.android.architecture.blueprints.todoapp.mock:id/design_navigation_view"
        // Open menu drawer.
        uiDevice.findObject(
                UiSelector().resourceId(toolbar))
                .getChild(UiSelector().className(ImageButton::class.java.name))
                .click()
        // Open Statistics section.
        uiDevice.findObject(
                UiSelector()
                        .resourceId(menuDrawer)
                        .childSelector(
                                UiSelector()
                                        .className(LinearLayoutCompat::class.java.name).instance(1)))
                .click()
        /**
         * Locate Statistics label based on the view id.
         * At this moment watcher kicks in and dismissed dialog by clicking on OK button.
         */
        val statistics: UiObject = uiDevice.findObject(UiSelector()
                .resourceId("com.example.android.architecture.blueprints.todoapp.mock:id/statistics"))
        // Assert expected text is shown.
        assertTrue("Expected statistics label: "You have no tasks." but got: ${statistics.text}",
                statistics.text == "You have no tasks.")
    }
    /**
     * Register Statistics dialog watcher that will monitor dialog presence.
     * Dialog will be dismissed when appeared by clicking on OK button.
     */
    private fun registerStatisticsDialogWatcher() {
        uiDevice.registerWatcher("StatisticsDialog", statisticsDialogWatcher)
        // Run registered watcher.
        uiDevice.runWatchers()
    }
    /**
     * Remove previously registered Statistics dialog.
     */
    private fun removeStatisticsDialogWatcher() {
        uiDevice.removeWatcher("StatisticsDialog")
    }
    companion object {
        private val instrumentation = InstrumentationRegistry.getInstrumentation()
        private val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
        val statisticsDialogWatcher = UiWatcher {
            val okDialogButton = uiDevice.findObject(By.res("android:id/button1"))
            if (null != okDialogButton) {
                okDialogButton.click()
                return@UiWatcher true
            }
            false
        }
    }
}

If we break it down, we will see that the UiWatcher instance is created first.

chapter8 .UiAutomatorUiWatcherTest.kt: Instantiating UiWatcher Object.
companion object {
        private val instrumentation = InstrumentationRegistry.getInstrumentation()
        private val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
        val statisticsDialogWatcher = UiWatcher {
            val okButton = uiDevice.findObject(By.res("android:id/button1"))
            if (null != okButton) {
                okButton.click()
                return@UiWatcher true
            }
            false
        }

Then, from the setUp() method that will be executed before each test run, we call registerStatisticsDialogWatcher() to register the watcher and run it.

chapter8 .UiAutomatorUiWatcherTest.kt: Registering and Running UiWatcher.
@Before
// Register dialog watcher.
fun before() = registerStatisticsDialogWatcher()
/**
 * Register Statistics dialog watcher that will monitor dialog presence.
 * Dialog will be dismissed when appeared by clicking on OK button.
 */
private fun registerStatisticsDialogWatcher() {
    uiDevice.registerWatcher("StatisticsDialog", statisticsDialogWatcher)
    // Run registered watcher.
    uiDevice.runWatchers()
}

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.

chapter8 .EspressoUiAutomatorTest. clickNotificationOpenMainPage() .
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
private val twoSeconds = 2000L
@get:Rule
var activityTestRule = ActivityTestRule(TasksActivity::class.java)
 /**
 * Clicks notification triggered by application under test and
 * verifies that TasksActivity is shown.
 */
@Test
fun clickNotificationOpensTasksActivity() {
    openDrawer()
    onView(allOf(withId(R.id.design_menu_item_text),
            withText(R.string.settings_title))).perform(click())
    onData(CoreMatchers.instanceOf(PreferenceActivity.Header::class.java))
            .inAdapterView(withId(android.R.id.list))
            .atPosition(1)
            .onChildView(withId(android.R.id.title))
            .check(matches(withText("Notifications")))
            .perform(click())
    // Click on Send notification item
    onData(withKey("notifications_send"))
            .inAdapterView(allOf(
                    withId(android.R.id.list),
                    withParent(withId(android.R.id.list_container))))
            .check(matches(isDisplayed()))
            .perform(click())
    // Perform UI Automator actions.
    uiDevice.openNotification()
    // Click notification by text and wait for application to appear.
    uiDevice.findObject(By.text("My notification"))
            .clickAndWait(Until.newWindow(), twoSeconds)
    // Verify application layout with Espresso
    onView(withId(R.id.noTasksIcon)).check(matches(isDisplayed()))
}

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.

chapter8 . EspressoUiAutomatorTest : Clicking on Delayed Notification By Its Text.
// Wait and click delayed notification by text.
uiDevice.findObject(By.res("com.android.systemui:id/notification_stack_scroller"))
        .wait(Until.findObject(By.text("My notification")), 8000)
       .clickAndWait(Until.newWindow(), twoSeconds)

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

Implementing a Practice Test Using Espresso and UI Automator
  1. 1.

    Implement a test using the UI Automator UiSelector that creates and then modifies the TO-DO item.

     
  2. 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. 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. 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.36.71