Leveraging Kotlin for Tests
Writing tests can be an important part of developing applications. Whether unit tests, integration tests or others, Kotlin’s language features and clever naming patterns can help us increase the readability of tests.
The patterns suggested here aren’t new or innovative; rather they are more modern adaptions of well-established practices. I learned about these adaptions from my lovely colleague Volodymyr Galandzij who did fantastic work in setting up the testing framework in a codebase we worked on together.
Given/When/Then
You may have heard of the Given/When/Then pattern — also known as the Cucumber/Gherkin syntax — if you have written tests before, or you might even recognize it from Jira tickets.
Originally introduced as a part of BDD (Behavior Driven Development), similar patterns like the Four-Phase Test (you’ll see some similarities to JUnit here) have existed for a good amount of time. Martin Fowler gives a good small summary of all of this in his article about the Given/When/Then pattern:
The essential idea is to break down writing a scenario (or test) into three sections:
The given part describes the state of the world before you begin the behavior you’re specifying in this scenario. You can think of it as the pre-conditions to the test.
The when section is that behavior that you’re specifying.
Finally the then section describes the changes you expect due to the specified behavior.
Regardless of the testing philosophy you follow, there’s a good chance that the Given/When/Then pattern can be applied to your tests. But how does this look in practice?
Test Names
I argue that test names are one of the most important parts of writing good tests. The name of the test should help you understand what the test does.
One quality of a good test is that it fails when the actual outcome is different from the expected outcome. When a test fails, it should be easy to understand why it failed. Think of a test failing on CI; it’s great if you can get a good understanding and maybe a first hunch from the test name already). You might know tests similar to this:
Applying the Given/When/Then pattern… doesn’t help us a ton:
Kotlin has an awesome language feature to help us with this: backtick (function) names¹. Using backticks allows writing freeform function names. This allows us to transform the test name to this:
Structuring Test Code
The Given/When/Then pattern can be applied in different ways, and all of them work well for different use cases. One of the approaches suggested by Martin Fowler is using comments to mark the regions of the test:
We like doing this in our codebase, but it could also be seen as clutter. Some people argue that your test regions should be clear in any case and that you should structure your code differently if you need comments to explain the structure. In our case, it helps us maintain separation between sections of the tests. It’s also easier to spot when parts of the test are out of place, i.e. an action that should be part of the “When” block is called in the “Given” or “Then” block.
Instead of using comments to mark the regions in your actual test code, you can use this practice to help you while writing the tests and remove the comments before submitting your change.
Providing a formal structure using a DSL
Test frameworks such as Spek help enforce a more formal structure using Kotlin DSLs. Take this test written with Spek’s Gherkin-inspired syntax:
You don’t need to adopt a full library for this though! A simple, homemade solution might work better and is more flexible. With this, you can also make tests fail when a “Then”-action is called in a “When” block or the other way around.
In practice, we stick with using code comments to mark the regions of the tests for the most part — we find it has less overhead when reading the code.
Expressing the “Given” scenario as a class
Regardless of whether you are following the Given/When/Then pattern or not, it is worth considering expressing the requirements of a test in a class. Say you need to provide a mock or a fake for your subject under test:
There are a few ways to make this code more reusable, but in any case, it’s a good idea to extract our fake LoginApi
implementation. By creating a class that holds our Given
scenario, we can encapsulate and re-use this:
From there, writing another test for a scenario with invalid credentials becomes very easy.
When working with more complex scenarios with a lot of parameters, representing the Given requirements as a model helps maintain a consistent style of testing and makes it easy to reuse tests.
More backtick functions… for Given scenarios!
Lastly, we can introduce some sugar on top of the Given
class to make expressing scenarios more fluent. We can make our Given
class a data class
to benefit from the copy
function and use extension properties to define scenarios:
This is not needed for simple tests like this, but for more complex test scenarios with varying input data, this can greatly help increase the readability. This allows us to re-use the scenarios, too.
Finally, we can use infix
extension functions for scenarios with inputs:
“Given“ Builders
Another approach to the Given is creating a builder for the Given
model. This works especially well when you are using a DSL but is useful for any scenario with complex inputs. Higher-order functions with a receiver can help create a nice DSL:
In conclusion, we can leverage some of Kotlin’s lesser-used language features to increase test code's readability, maintainability, and reusability. While not all of them work for every environment, it is always good to take some time to think about how we can write tests in developer-friendly ways without sacrificing test quality.
Footnotes
¹ There is a caveat to this language feature though: it’s not available on Android platforms before SDK 30, so if you are writing instrumented tests and are not ready to go to minSdk
30 for your tests (understandably), you won’t be able to use this feature. Backtick function names work on JVM targets though, so these make sense especially for unit tests. But some other patterns can be used to improve your test code!