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

11. The Screen Object Design Pattern in Android UI Tests

Denys Zelenchuk1 
(1)
Zürich, Switzerland
 

The Screen Object Design Pattern in mobile UI tests is equivalent to the well known Page Object Design Pattern in web tests, which is the abstraction layer representing an interface that allows its users to operate page elements or validate the page state. Since Page Object takes its name from the web page, it is hard to name a mobile application View or Screen, which is represented to the users as a page. This chapter demonstrates how the Screen Object Design Pattern can be applied to Android UI tests using Kotlin. You will learn to create a screen object that represents a single Android application activity or fragment (i.e., a screen) and then use these objects or their methods in tests that represent real user flows.

Pros and Cons of the Screen Object Design Pattern in Android Test Projects

When a screen object is defined, it contains a set of methods that are used in tests and represent the screen functionality or specific screen state validation. From the test execution side, this eliminates the need to write step-by-step test instructions in favor of calling those methods.

Pros

Let’s look at the benefits to this approach:
  • Logical test steps separation

  • More readable tests

  • Easy-to-build user flows

  • Easily maintainable tests

  • Code reuse

Logical Test Steps Separation

A test step refers to the functional user action that sometimes may consist of multiple test interactions with the application being tested. For example, adding new TO-DO to the sample TO-DO application consists of three actions: typing title, typing a description, and clicking the Done button.

More Readable Tests

Based on these logical steps, after all the screens are defined, we will end up having a set of these steps that have easy-to-understand names. So, even inexperienced test engineers can easily understand the test flow’s logic.

Easy-to-Build User Flows

The user flow here is the set of chained test methods that can represent one screen or navigate from one screen to another, thereby replicating the end user behavior. They are super useful to understand the end user flow’s test coverage.

Easily Maintainable Tests

Since all the screen elements declarations are located in one class, it reduces the amount of maintenance effort when there is application refactoring. Imagine a situation in which the login button is identified by its ID. This button is used in multiple tests and is clicked by the Espresso onView(withId(R.id.loginButton)).perform(click()) code. Then the ID of the button changes, which leads to updating all the code lines where it is used. Having, let’s say, a LoginScreen class that contains a Login button declaration and implements the login method makes the change done only in one place—the LoginScreen.

Code Reuse

This point is very similar to the previous one, because of the fact that view elements are encapsulated in the screen or their methods and usually are not accessible by other screens. That means they can be reused in multiple test flows by calling screen methods that contain screen elements.

Cons

Here are some cons to using the Screen Object Design Pattern:
  • There is no clear way to handle views used across different screens

  • The same action may open different screens depending on the navigation stack

  • Too detailed screen methods lead to long tests

Handling Views Across Different Screens

As mentioned, there is no clear way to handle views or view groups used across different screens. Android mobile applications have shared and reusable components across many application screens and this impacts and challenges this test design pattern. Here we are talking about such views as menu drawers or tab bars. Figure 11-1 shows two of the TO-DO application screenshots.
../images/469090_1_En_11_Chapter/469090_1_En_11_Fig1_HTML.jpg
Figure 11-1

Menu drawer opened from the TO-DOs list screen (left side) and from the Statistics screen (right side)

Both of them have the same menu drawer component. Now where should this menu drawer component be declared? Should it belong to each screen duplicating the test source code? We will see later how this case can be handled.

Same Action Opening Different Screens

Recall that the same action may open different screens depending on the navigation stack. In Android applications, depending on the activity navigation stack or on the application logic, the same action on one screen may have a different end result based on the navigation flow prior to the current application state. A good example of such a case is the TO-DO application Back or Up button click navigation from the Settings section. Let’s try two flows:
  1. 1.

    Open the Settings section from the TO-DO list screen. From the Settings screen, click the Up button.

     
  2. 2.

    Open the Settings section from the Statistics screen. From the Settings screen, click the Up button.

     
As you can see in Figures 11-2 and 11-3, the Up button is used in both cases, but the end result is different. In the first case, we navigate back to the TO-DO list screen. In the second case, we navigate back to the Statistics screen.
../images/469090_1_En_11_Chapter/469090_1_En_11_Fig2_HTML.jpg
Figure 11-2

Open Settings and click Up from TO-DO list screen

How do we deal with such a case? We will see later in the chapter one possible solution.
../images/469090_1_En_11_Chapter/469090_1_En_11_Fig3_HTML.jpg
Figure 11-3

Open Settings and click Up from the Statistics screen

Detailed Screen Methods Lead to Long Tests

Sometimes test engineers tend to be too detailed when creating screen classes and their methods. They try to wrap almost each single action or verification into a method, dramatically increasing the number of steps inside each test. We should try to always find a golden middle—on the one side having the proper logical split without making screen methods too small and detailed; and on the another side we should not put a lot of screen actions or verifications into a small number of methods.

Applying the Screen Object Design Pattern

It is a time to switch to the example. First, we will look at how a single TO-DO application screen can be implemented, including its visual representation. Figure 11-4 breaks down the New TO-DO screen into functional sections.
../images/469090_1_En_11_Chapter/469090_1_En_11_Fig4_HTML.jpg
Figure 11-4

The New TO-DO screen broken into functional screen object elements

As Figure 11-4 shows, the New TO-DO screen contains six functional or actionable elements. Each of these elements represents a single method in the AddEditToDoScreen . For example, the typeToDoTitle method will look the following way:
class AddEditToDoScreen {
    private val addToDoTitleEditText = onView(withId(R.id.add_task_title))
    fun typeToDoTitle(title: String): AddEditToDoScreen {
        addToDoTitleEditText.perform(typeText(title), closeSoftKeyboard())
        return this
    }
}

Performing an action on elements two and three will keep users on the same screen; therefore, their methods will return the same AddEditToDoScreen instance.

In a similar way, methods for other elements are created. The rest of them redirect the user to different application screens. This is the example of a method that clicks the Done floating action button that returns a ToDoListScreen instance :
class AddEditToDoScreen {
    private val doneFabButton = onView(withId(R.id.fab_edit_task_done))
    fun clickDoneFabButton(): ToDoListScreen {
        doneFabButton.perform(click())
        return ToDoListScreen()
    }
}

Here, the clickDoneFabButton() method returns an instance of ToDoListScreen according to the application flow. We should also include verification methods that validate the screen in a specific state and system back action.

And one more thing—single screen actions may be too detailed for the test step and logically may be grouped into a set of screen actions. A good example is when adding new TO-DO flows that consist of three actions: typing a title, typing a description, and clicking a floating action button. Let’s look at the final ToDoListScreen implementation state.

chapter11.screens.AddEditToDoScreen.kt.
class AddEditToDoScreen : BaseScreen() {
    private val addToDoDescriptionEditText = onView(withId(R.id.add_task_description))
    private val addToDoTitleEditText = onView(withId(R.id.add_task_title))
    private val doneFabButton = onView(withId(R.id.fab_edit_task_done))
    private val emptyToDoSnackbar = onView(withText(R.string.empty_task_message))
    private val upButton = onView(allOf(
            instanceOf(ImageButton::class.java),
            withParent(withId(R.id.toolbar))))
    fun typeToDoTitle(title: String): AddEditToDoScreen {
        addToDoTitleEditText.perform(typeText(title), closeSoftKeyboard())
        return this
    }
    fun typeToDoDescription(description: String): AddEditToDoScreen {
        addToDoDescriptionEditText.perform(typeText(description), closeSoftKeyboard())
        return this
    }
    /**
     * Represents adding new to-do flow
     */
    fun addNewToDo(taskItem: TodoItem): ToDoListScreen {
        typeToDoTitle(taskItem.title)
        typeToDoDescription(taskItem.description)
        clickDoneFabButton()
        return ToDoListScreen()
    }
    fun addEmptyToDo(): AddEditToDoScreen {
        clickDoneFabButton()
        return this
    }
    fun clickDoneFabButton(): ToDoListScreen {
        doneFabButton.perform(click())
        return ToDoListScreen()
    }
    fun clickUpButton(): ToDoListScreen {
        hamburgerUpButton.perform(click())
        return ToDoListScreen()
    }
    fun clickBackButton(): ToDoListScreen {
        Espresso.pressBack()
        return ToDoListScreen()
    }
    fun verifySnackbarForEmptyToDo(): AddEditToDoScreen {
        emptyToDoSnackbar.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
        return this
    }
}

In this code snippet, you can see that the AddEditToDoScreen class extends the BaseScreen class , which can contain common screen elements like the Up button ViewInteraction in our case.

In this same way, other applications screens can be created. When this is done, we can start writing UI tests. To tell the truth, it now becomes really easy—test steps are chained based on the logical functional flows.

chapter11 . tests.AddToDoTest.kt .
/**
 * Validates TO-DOs creation flows using Screen Object Pattern.
 */
class AddToDoTest : BaseTest() {
    @Test
    fun addsNewTodo() {
        ToDoListScreen()
                .clickAddFabButton()
                .addNewToDo(todoItem)
                .verifyToDoIsDisplayed(todoItem)
    }
    @Test
    fun addsNewTodoWithoutDescription() {
        ToDoListScreen()
                .clickAddFabButton()
                .typeToDoTitle(todoItem.title)
                .clickDoneFabButton()
                .verifyToDoIsDisplayed(todoItem)
    }
    @Test
    fun triesToAddEmptyToDo() {
        ToDoListScreen()
                .clickAddFabButton()
                .addEmptyToDo()
                .verifySnackbarForEmptyToDo()
    }
    companion object {
        private var todoItem = TodoItem()
        @Before
        fun setUp() {
            todoItem = TodoItem.new
        }
    }
}
Returning to the advantages of the Screen Object Design Pattern, we can see that all of them are covered:
  • Logical test steps separation—Achieved by splitting actions per screen and creating functional flows like the addNewToDo(...) method.

  • More readable tests—With the current implementation, it is clear from the test where we start and what exact actions are performed.

  • Easy-to-build user flows—Having a set of screens returning their public results makes writing test cases easy.

  • Easily maintainable tests—Achieved by isolating element declaration inside the screen class. So, there is no need to update them in multiple places after the application is refactored.

  • Code reuse—As you can see, the screen methods can be reused by any test without the need to replicate the same or similar code.

We also covered one negative point:
  • Too detailed screen methods lead to long tests—This issue should be solved by creating functional flows as was shown by the addNewToDo(...) method. Instead of writing many steps belonging to the same screen, we group them into one method. Keep in mind that functional flows ideally should be isolated per screen; otherwise, it will be hard to understand test steps or analyze test failures.

Now we will analyze the case shown in Figure 11-1 where the menu drawer view was used across different TO-DO application screens (ToDoListScreen, StatisticsScreen, etc.). In this situation, we have two options—we can duplicate the code in each screen (which we don’t want) or we can create the new class similar to the screen but that will represent the common view. Since it doesn’t represent the screen itself, we will call it MenuDrawerView. In the example application, it is implemented inside the BaseScreen class.

chapter11 . screens.BaseScreen.MenuDrawerView inner class .
/**
 * Base screen that shares common functionality for main application settings
 * like TO-DO list screen and Statistics screen.
 */
open class BaseScreen {
    private val hamburgerButton = onView(allOf(
            instanceOf(ImageButton::class.java),
            withParent(withId(R.id.toolbar))))
    fun openMenu(): MenuDrawerView {
        hamburgerButton.perform(click())
        return MenuDrawerView()
    }
    inner class MenuDrawerView {
        private val todoListMenuItem = onView(allOf(
                withId(R.id.design_menu_item_text),
                withText(R.string.list_title)))
        private val statisticsMenuItem = onView(allOf(
                withId(R.id.design_menu_item_text),
                withText(R.string.statistics_title)))
        private val settingsMenuItem = onView(allOf(
                withId(R.id.design_menu_item_text),
                withText(R.string.settings_title)))
        private val todoMenuLogo = onView(withId(R.id.headerTodoLogo))
        private val todoMenuText = onView(withId(R.id.headerTodoText))
        fun clickTodoListMenuItem(): ToDoListScreen {
            todoListMenuItem.perform(click())
            return ToDoListScreen()
        }
        fun clickStatisticsMenuItem(): StatisticsScreen {
            statisticsMenuItem.perform(click())
            return StatisticsScreen()
        }
        fun clickSettingsMenuItem(): SettingsScreen {
            settingsMenuItem.perform(click())
            return SettingsScreen()
        }
        fun verifyMenuLayout(): MenuDrawerView {
            todoMenuText.check(matches(allOf(
                    isDisplayed(),
                    withText(R.string.navigation_view_header_title))))
            statisticsMenuItem.check(matches(isDisplayed()))
            todoListMenuItem.check(matches(isDisplayed()))
            return this
        }
    }
}

Now we address the last problematic moment we mentioned—the same action may open different screens depending on the navigation stack shown in Figures 11-2 and 11-3. Here we will use simplest solution and add multiple methods with the same functionality. The only difference is in the type of returned screen.

chapter11 . screens.SettingsScreen.kt .
class SettingsScreen {
    private val upButton = onView(allOf(
            instanceOf(AppCompatImageButton::class.java),
            withParent(withId(R.id.action_bar))))
    fun navigateUpToToDoListScreen(): ToDoListScreen {
        upButton.perform(click())
        return ToDoListScreen()
    }
    fun navigateUpToStatisticsScreen(): StatisticsScreen {
        upButton.perform(click())
        return StatisticsScreen()
    }
}

Here is the test case implementation.

chapter11 . tests.SettingsTest.verifiesUpNavigation() .
/**
 * Validates TO-DOs application Settings functionality.
 */
class SettingsTest : BaseTest() {
    /**
     * Validates application UP button navigation from Settings screen.
     */
    @Test
    fun verifiesUpNavigation() {
        ToDoListScreen()
                .openMenu()
                .clickSettingsMenuItem()
                .navigateUpToToDoListScreen()
                .verifyToDoListScreenInitialState()
                .openMenu()
                .clickStatisticsMenuItem()
                .dismissAlertDialog()
                .openMenu()
                .clickSettingsMenuItem()
                .navigateUpToStatisticsScreen()
                .verifyStatisticsScreenInitialState()
    }
}

Exercise 26

Writing Tests Using the Screen Object Design Pattern
  1. 1.

    Create screen classes for all the application activities and fragments.

     
  2. 2.

    Write at least one test per created screen.

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

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