Write Tests You Love, Not Hate #2 - Increase test readability by mastering the basics
Writing tests is not the greatest joy for most developers. In this series, we look into the problems of testing and offer an alternative approach used at Chrono24 to make tests both useful and simple.
Let’s face it: Writing tests can often be painful and cumbersome. Tests can feel more like a hindrance than a support to engineers. In this series, we look into the problems of testing and offer an alternative approach used at Chrono24 to make tests both useful and straightforward.
We identified the main obstacles to effective testing in the last post:
Excessive boilerplate code makes tests hard to write and to grasp quickly.
The Fragile Test Problem leads to unnecessary work and results in unreliable tests.
Tests that take too long to execute create friction in the workflow.
In this post, we work on the readability of tests by getting the basics right. To that end, let's revisit our example:
The test (see Figure 1) configures a set of mocked objects (yellow part), calls a component under test (green part), and verifies the result (pink part). The test is difficult to read and more complex than necessary. Figure 1 already summarizes the steps at a higher level, such as 'set next user ID' and 'verify user saved.' These can guide us when extracting methods to improve readability. However, before we delve into that, let's revisit some established patterns for structuring tests.
Given / When / Then
You might be familiar with the Given/When/Then pattern for unit tests, which essentially covers the three parts of a test:
Given: The test setup including the configuration of mocked objects.
When: The call to the component under test (CUT).
Then: The verification of test results.
Many people have written about this pattern, including Martin Fowler. It's also part of behaviour driven development (BDD) introduced by Dan North and is utilized in Cucumber.
By applying this pattern and extracting a set of methods, we can significantly increase the readability of the test.
Figure 2 shows the result. Essentially, we introduced a method for each block that we identified earlier. For example, the code block that sets the next user ID in the UserRepository:
when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
User user = invocation.getArgument(0);
user.setId(27);
return user;
});
becomes:
givenNextUserId(27);
which is much more readable. Please note that all of the method names start with 'given', 'when', or 'then', allowing engineers to quickly identify the correct methods for each step of a test. With this naming scheme, the available options immediately become visible in the IDE’s code completion. This pattern not only keeps the tests clear but also eliminates the need for unnecessary comments, which can detract from readability.
Explicit Relationship between Setup and Verification
With the refactoring above, the test has become much clearer and easier to understand. However, the relationship between setup and verification is still not immediately apparent. For example, it’s challenging to identify where the activation code is used in the verification or whether the user ID is included in the generated URL.
To make such dependencies explicit, we can extract input parameters like the user ID and the activation code as constants and connect them to their usage in the verification. As demonstrated in Figure 3, this approach immediately clarifies that the activation code is stored in the user object and included in the generated URL. The use of the user ID as part of the URL also becomes directly visible.
We are not done yet
Extracting methods from our original test in Figure 1, following the Given/When/Then pattern, and making the dependencies between the test setup and verification explicit have already significantly improved the readability of our test. However, we are not done yet. Even though the test is much easier to understand now, the entire test class is still a mess.
In Figure 4, you can see the problem very clearly. The test is just a small fraction of the test class. The rest of the class is boilerplate code to set up the tests. We've extracted a really nice set of methods for 'given', 'when', and 'then', but they are still part of the test class and only available for local use.
In the next post, we will explore how we can employ Entity Builders
to reduce the boilerplate code needed for the test setup.
Thank you for reading ❤️