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
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
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
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
- 1.
Open the Settings section from the TO-DO list screen. From the Settings screen, click the Up button.
- 2.
Open the Settings section from the Statistics screen. From the Settings screen, click the Up button.
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
Performing an action on elements two and three will keep users on the same screen; therefore, their methods will return the same AddEditToDoScreen instance.
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.
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.
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.
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.
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.
Here is the test case implementation.
Exercise 26
- 1.
Create screen classes for all the application activities and fragments.
- 2.
Write at least one test per created screen.