At Shazam we like to test. We have hundreds of acceptance tests written using Android’s Espresso framework. Espresso is great because it makes it simple to write automated tests, but writing raw Espresso code in your tests can quickly become difficult to maintain.
Imagine we have a very simple screen which looks like this:
We have one Button
to login/logout and a TextView
to tell the user if they’re logged in or out.
We want to write a test that makes sure that when you click “Log Out”, the text updates to reflect that. This is simple enough to write using Espresso:
- We update shared preferences to make sure that the user is logged out before the test starts
- The user launches the app
- The user clicks on “Log In”
- We assert that the user sees the logged in text
This is okay for one test, but what about when we have more tests? If we were to write a test for logging in, we’d have to duplicate some of our code, such as pressing on the “Log In/Out” button. Any test that needs the user to be logged out will duplicate the code to setup the device as logged out. The more tests we add, the more duplicated code we will see.
So, how can we overcome this? The majority of our acceptance tests can be boiled down to setup, actions and assertions. Or in other words: given
, when
, then
. So why don’t we try writing out tests like that?
- Given the user is logged in
- When the user launches the app
- When the user clicks “Log Out”
- Then the user sees the logged out text
We can translate this into code by writing methods to encapsulate these actions — and this is exactly what we do at Shazam. We even have an open source library to help with the syntax called Gwen. We can write our test, using Gwen, in Java, like this:
This is great. We have reusable methods for our tests. Even better, if we change the implementation, for example replace shared preferences with an sql database, we don’t have to go through all of our tests to update them. We would only have to change one method, has().loggedOut()
.
We’ve been using this pattern for years and it’s helped us keep high test coverage and build a robust, stable app, while keeping developer time writing tests to a minimum.
Now that we’re using Kotlin, we wanted to see if there were any ways that we could improve our acceptance tests, particularly using Kotlin’s DSLs and infixed functions. We want to keep the same ‘given, when, then’ language of our tests but improve the syntax using Kotlin.
We can make a DSL using Kotlin to let us write the same test, like this:
The code for ‘given, when, then’ looks like this:
We have an infix, extension function which accepts a function block and executes it. The function is an extension function on Any
, so that we can call it on any type. It’s also infix so that we don’t need the .
to call it and we can have spaces between what we’re calling and the function block parameter. What this lets us do is chain the ‘given, when, then’ calls like a sentence and gets us one step closer to our DSL.
Next, for our actors we do something similar. In our code base we have all of our actors, such as user
, os
, server
etc, as protected fields in a base class. This lets us call them by their name. For example, in java, user.selects().logIn()
. For our kotlin DSL we want to make something more idiomatic. Something like this: user selects { login() }
And the selects
function looks as follows:
This is an infix function on User
. It takes a function with SelectsActions
as the receiver, letting us call functions on SelectsActions
in the lambda passed in. We invoke the function and return the User
so that we can chain actions. The whole function is an infix function so that we can have spaces and makes the call read more like a sentence.
This just leaves the actions and assertions of the test. This is where the actual Espresso code lives, as below:
When you put all of the above pieces together you can write nice, human-readable tests. And even better, our actual Espresso code is completely reusable since it’s abstracted behind our Actors
.
You can take a look at a demo project using these techniques here: https://github.com/shazam/GWEN-Kotlin-Demo