How to make automated tests flexible and concise

Arseny Fedorov
Kaspersky
Published in
8 min readNov 23, 2023

--

Streamlined architecture is as essential to automated tests as it is to the main code. Redundancy and inflexibility can cause issues: any changes in the UI will necessitate updates to multiple files, tests may functionally duplicate one another, and supporting new features may turn into a lengthy, challenging effort to adapt existing tests.

My name is Arseny Fedorov. I am a Software Development Engineer in Test on the Kaspersky Internet Security for Android team. In this article, I will show a different approach to development of automated tests and a number of best practices which will help you to avoid the aforementioned problems.

How the page object pattern straightens out code

When writing tests for an application, we need to refer to the application’s view elements when running various checks or actions. If we always explicitly state element IDs in each test we write, this will make our code susceptible to UI changes: we will have to update all IDs that have changed in each test that uses them.

The page object pattern helps to prevent this. The idea is to present the page (application screen) as an object (test abstraction) that announces and initializes all graphic elements on the page and sets up interaction with them. Detailed information about the pattern can be found here.

All examples in this article use Kaspresso, our open-source test automation framework. Why not Espresso?

First, Kaspresso uses a declarative approach to writing tests that relies on Kakao, a Kotlin DSL wrapper over Espresso. Here’s an example:

Espresso

Kaspresso

Second, Kaspresso improves stability by avoiding flaky tests with the help of interceptors. These come in especially handy when we are working on asynchronous graphic elements or lists.

Third, Kaspresso incorporates KAutomator, a convenient Kotlin DSL wrapper over UI Automator that speeds up UI tests. You can see the difference between the standard (right) and accelerated (left) UI Automator in action below:

Besides this, Kaspresso allows you to break tests down into steps, similarly to how this is done with manual test cases, and log each step. If the test crashes, the log will help you to immediately see which steps were completed successfully and which one failed. In addition to the logs, you have access to a hierarchy of graphical elements, as well as videos, screenshots, etc. Android Debug Bridge (adb) support built into Kaspresso will help you to work with Android directly. Allure integration provides a clear display of test results. You can read about the benefits of using Kaspresso in more detail here.

So, let us get down to business by trying to automate the Tutorial application. You can reproduce all the steps described below by downloading the project source and running it. We will describe the MainActivity page and automate LoginActivity testing. The results, along with the tests, are available in the TECH-tutorial-results branch, so you can go there any time and look at the finished code.

MainActivity looks like this:

We create a MainScreen object that inherits from KScreen:

KScreen implements the page object pattern, which describes all the view elements that the test interacts with.

The page object implementation in Kaspresso is notable for the layoutId and viewClass variables, which help developers immediately recognize which layout file is used for the page in question and which class provides its functionality. But the task at hand is to discuss the page object concept itself, so we set these to null for now.

We use UI Automator Viewer or Layout Inspector in Android Studio to find the ID of the Login Activity button. The identifiers of the rest of the view elements on the page can be found similarly.

This is what the description of the MainScreen elements looks like:

Now we can refer to the MainScreen object from any test we create and work with the view elements of this page.

Let’s write our first test, which will check if there’s a Login Activity button on the page and click it.

To that end, we create a LoginActivityTest class that inherits from TestCase:

As you can see in the test code, after we create the MainScreen object, we can use a few lines to refer to page elements, run the checks we need, and click on the button. The test ends with the LoginActivity page open. We look at its layout…

…and create LoginScreen:

Let’s modify LoginActivityTest and try getting authorized with the login “123456” and password “123456”:

Once authorized, we are greeted by the last page, AfterLoginActivity.

Kaspresso can check from inside the test what activity is being displayed by using the Device class. We conclude this first test by checking that AfterLoginActivity appears on the screen of the device following the authorization:

This is what our code would look like if we did not use the page object pattern:

This approach makes it harder to realize on the fly what test strings interact with what pages. Adding new checks and actions can make the code illegible. Therefore, we recommend using a page object to create quality scalable tests.

Breaking the test down into steps

Any test, whether automatic or a manual one, follows a test case — that is, the tester has a sequence of steps that they check to see if the page is fully functional. Kaspresso breaks code down into steps with the help of the step() function. Steps also help to organize test logs.

To use steps, you need to call the run{} method inside the test and list between the curly brackets all the steps to be run as part of the test. Each step should be called inside a step() function.

Let’s try this:

Thanks to the steps, the INFO-level logs tagged “KASPRESSO” look like this:

If you still have questions about steps, I suggest you read this article. It also provides the details of the Before/After sections you may have noticed in the logs.

Now, let’s try implementing negative test cases, such as the user entering a login or password that contain less than the minimum number of characters (six).

When creating a group of automated tests, a rule to follow is to have a separate test method for each test case. In other words, we will not test the behavior when entering an invalid login or password in the same method but create separate methods in the LoginActivityTest class:

Another test, with a valid login and invalid password:

I suggest renaming the first test while you are at it, so that its name shows that we are checking only for successful authorization.

We change it to:

You may have noticed that in the automated tests above, the strings for navigating to the LoginActivity page and entering login credentials repeat. It would be nice to reuse those steps!

Using scenarios

Kaspresso contains a tool named Scenario, which allows combining several steps into an ordered sequence of actions. This proves helpful when writing tests where the steps repeat.

Let’s create a LoginScenario class that inherits from Scenario. To make it work, we need to override the steps property to list all steps in the scenario.

The problem here is that we did not initialize the username and password variables. We can fix this by designating these as parameters in the LoginScenario class constructor. Then, this part of the code:

Changes to:

Here’s the resulting scenario code:

Let’s use this scenario in our LoginActivityTest tests:

We have looked at one case that favors the use of scenarios — when the same steps are reused in different tests of the same page. That is, however, not the only purpose of scenarios.

An application can have multiple pages that you can only access as an authorized user. You would then need to describe the authorization steps for each page anew. However, if you use scenarios, this turns into a very simple job.

Currently, the AfterLoginActivity page opens after we log in. Let’s write a test for that screen.

First we create a page object:

Then we add the test:

We need to get authorization to get to the page. Without scenarios, we would have to rerun all the steps again: open the main page, click the button, enter the login and password, and click the button again. This whole process is now reduced to using LoginScenario:

Scenarios like that can be invoked inside other scenarios, too. You can read more on scenarios here.

To sum up, using scenarios makes code clean, clear, and reusable. You don’t need to repeat a large number of identical steps any more if you want to test pages that are only accessible to authorized users. Importantly, we have also achieved proper test scalability. If the identifiers of the UI elements on the LoginActivity page changed, this will not require updates to the test code. All you will need to do to make the tests work again is fixing LoginScreen.

For contrast, here is the test code without the best practices. I hope you will forget this writing style like a bad dream.

Conclusion

Today, we have learned to write flexible, scalable, easily maintained automated tests by using the page object pattern and certain handy features of Kaspresso. If this got you interested in the Kaspresso framework, and you would like to make it part of your project, follow this link for a tutorial with all the details.

You can also browse the Kaspresso source code in GitHub. Your GitHub stars would be appreciated, and if there’s anything you’d like to improve about Kaspresso, join the ranks of contributors.

You can talk to the Kaspresso developers and ask any questions you want by joining the Kaspresso community on Discord.

And futhermore, we welcome you to read our previous publications about autotests on Android:

A step-by-step tutorial in codelab format for Android UI testing

How to make Espresso tests more readable and stable

--

--