This is the third and final part of the Android testing blog post series and today we’ll talk about the Robot pattern and how to use Kotlin to improve it.
Android testing (Part1): AWS Device Farm vs Firebase TestLab
Android testing (Part2): Kotlin DSL for Espresso and UIAutomator
Android testing (Part3): The power of the Robot pattern with Kotlin
In Part 2 of this blog post series, we have discussed and shown a Kotlin DSL library we developed for the Espresso and UIAutomator, as we noticed that readability and maintainability are endangered and made a necessary steps. However, the DSL is only one step into the right direction.
The DSL doesn’t solve the separation of concerns at all. Improvement of the readability was still needed. We addressed these issues with the Robot pattern. As a beneficial side-effect, we hide the library and the DSL syntax completely.
What is the Robot pattern? And for that matter, what is the robot?
Robot is a simple class dedicated to one screen in the application. It contains the implementation of use cases for the interaction with the UI components of that screen only. We’re making the robot which mimics the user interaction for a single screen.
Let’s take a simple login screen with username and password fields and login button as an example. For this login screen, we would create a LoginRobot class. In this class we will implement a single function:
class LoginRobot {
fun login(username: String, password: String) {
//finds all UI elements, interact and log in user.
}
}
Click on the button to load the content from giphy.com.
When used in the test, we will instantiate the LoginRobot class and call a login function, providing the username and password. So we will have something like this:
@Test
fun shouldLoginToTheApp() {
val loginRobot = LoginRobot()
loginRobot.login("john_smith", "p@$$w0rd")
}
However, the snippet above doesn’t do any assertion, therefore, the test is pointless. Also, implementing the assertion directly in the test doesn’t make too much sense, as we’re trying to extract the logic into Robot-like classes.
What is a RobotResult?
Usually, each Robot has its own RobotResult class. RobotResult is a class which holds the assertions, per use case, for a single screen. In our case, besides LoginRobot, we will have a LoginRobotResult.
Our LoginRobotResult will have a function isLoggedIn().
class LoginRobotResult {
fun isLoggedIn() {
//contains the assertion for login status
}
}
Similarly, as with LoginRobot, we have to instantiate the LoginRobotResult and use it in the test.
@Test
fun shouldLoginToTheApp() {
val loginRobot = LoginRobot()
loginRobot.login("john_smith", "p@$$w0rd")
val loginRobotResult = LoginRobotResult()
loginRobotResult.isLoggedIn()
}
Comparing to the direct approach, where we find the UI elements in the test function body and implement the interaction and assertion one below the other, this looks much better and understandable. But, we wouldn’t be here just to show you simple separation and wrapping the logic into two classes, right? 🙂
Below, we’ll show you how to improve the readability with Kotlin infix notation.
Kotlin infix, extension and higher-order functions in a mission to boost the readability to max
By moving the interaction logic into the Robot class and by moving the assertion logic into the Robot result class, we made necessary steps to improve the maintainability. We have basically applied the separation of concerns principle on the test.
To improve the readability, as a first step we might avoid direct instantiation of classes in the test example above. Instead, by creating a top level higher-order function, we will shorten the login interaction to single expression only. The function we will create is called withLoginRobot (this naming convention increases the semantic of test body). This function creates a LoginRobot instance and accepts the lambda in the context of LoginRobot.
fun withLoginRobot(fn: LoginRobot.() -> Unit): LoginRobot
= LoginRobot().apply(fn)
The test looks more readable now:
@Test
fun shouldLoginToTheApp() {
withLoginRobot {
login("john_smith", "p@$$w0rd")
}
val loginRobotResult = LoginRobotResult()
loginRobotResult.isLoggedIn()
}
With the identical approach, we can create a function called verifyThat to instantiate a LoginRobotResult class.
fun verifyThat(fn: LoginRobotResult.() -> Unit): LoginRobotResult
= LoginRobotResult(fn)
Which improves the test a little bit as well:
@Test
fun shouldLoginToTheApp() {
withLoginRobot {
login("john_smith", "p@$$w0rd")
}
verifyThat {
isLoggedIn()
}
}
Although this looks cool, there is more space for improvement. By using the infix notation and making our verifyThat function an extension function of LoginRobot, we’ll be able to make a sequential call and single expression of the whole test content.
infix fun LoginRobot.verifyThat(fn: LoginRobotResult.() -> Unit): LoginRobotResult
= LoginRobotResult(fn)
Finally, we have a desired look of our test.
@Test
fun shouldLoginToTheApp() {
withLoginRobot {
login("john_smith", "p@$$w0rd")
} verifyThat {
isLoggedIn()
}
}
For the sake of readability, additionally we can rename verifyThat into andVerifyThat and rename isLoggedIn into userIsLoggedIn(). This is a subjective decision, however, we can read this test very easily, in a natural, human-readable way:
“With login Robot, login John Smith and verify that user is logged in”.
On the first read, it’s very understandable what this test do and that’s exactly what we want to achieve.
UI interaction in multiple steps
UI tests with only one interaction step are very rare. Usually, we have to do multiple navigation steps throughout the app, to put it into the desired state ahead of the assertion.
For example, first we have to login, then we have to accept the terms of service and then to accept required permissions.
Let me introduce you the andThen and andThenWith functions.
The role of these functions is to wire the expression into a single body, with the possibility to introduce an additional interaction step with the same Robot or with some other Robot.
infix fun LoginRobot.andThen(fn: LoginRobot.() -> Unit): LoginRobot {
also(fn)
}
or:
infix fun LoginRobot.andThenWithPermissionRobot(fn: PermissionRobot.() -> Unit): LoginRobot {
PermissionRobot().apply(fn)
return this
}
Whatever option we decide to use, our test will stay readable as it was:
@Test
fun shouldLoginToTheApp() {
withLoginRobot {
login("john_smith", "p@$$w0rd")
} andThen {
acceptTermsOfUse()
} andThenWithPermissionRobot {
acceptAllPermissions()
} andVerifyThat {
userIsLoggedIn()
}
}
Click on the button to load the content from giphy.com.
Isn’t this awesome 🙂 !
Recap
With this approach, we’ve created a couple of abstraction layers for the UI testing with Robot classes and Kotlin DSL a building blocks.
- Each screen has its own Robot.
- Each robot has its own Robot result.
- withRobotName function is used to initialise the Robot.
- andThen function is used to wire the calls together and increase the semantic of the expression.
- verifyThat function in Robot result is used for the assertion implementation.
- A combination of infix notation with higher-order extension functions helps us create a single readable expression.
- AndroidTestKTX from Part2 is used within the Robot functions to simplify the UI interaction.
Android UI testing should be fun as well and with this approach we’re a couple of steps closer.
I wish you joyful testing!
More articles
fromDusko Bajic
Your job at codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
More articles in this subject area
Discover exciting further topics and let the codecentric world inspire you.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog author
Dusko Bajic
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.