From my observation, code bases with a high test coverage and a certain level of complexity tend to have cluttered and messy tests. Such tests are often dominated by the preparation step in order to prepare test data or bring the system into a state required for the test. The actual test code, such as invocation of the component-under-test and verification of the results, can easily be buried under the preparation step.
To ease such problems and to improve readability and maintainability of test code in general, I like to combine the Builder pattern with the Object Mother pattern and randomized test data.
Test cases can often be separated into three logical steps:
- given step: Prepares the data, models and state required for the test to run,
- when step: actual execution of the component-under-test,
- then step: verification of the results.
The advise given in this article refer mostly to the given step.
Builder Pattern
From my experience, the Builder pattern is widely known. It also supported by Lombok via the @Builder
annotation. In this article, I will not get into the details of this pattern.
Object Mother Pattern
On the other hand, the Object Mother pattern is not widely known. It builds on the concept of a builder, but provides more complex builder methods. Such methods usually change more than one field of the underlying model. What fields are changed depends strongly on the domain and use cases of the model.
For more details, please see the Martin Fowlers article on Object Mother.
Randomized Test Data
Before executing the component-under-test, the test data preparation is critical because the actual values used for the test affect the results. This is the obvious and deliberate part. But usually, there are also many values and preparations that just need to be done beforehand but have no influence on the test result as such. Examples are mandatory fields in models which are not used in the tested scenario, or dependencies that have to be mocked to not run into errors during the execution.
Since the actual values for such secondary test data has no effect on the test result, I propose to generate such data randomly. This has a few benefits that, in my opinion, outweigh any downsides:
- Clear separation of primary (which has a direct influence on the test result) and secondary (which has no influence on the test result) test data.
- Since the secondary test data is randomized, different values and combinations (think of empty lists, optional values, etc.) are tested over time.
- Previous assumptions are constantly challenged with each test execution. This also holds on an evolving code base in which tests can easily become obsolete or useless by later changes.
An alternative to random test data can be mocked domain objects. This can also work well, but can be hard when working with nested data and complex models.
Example
Lets use this example domain models to demonstrate the advantages. Keep in mind that these models are neither realistic nor complete.
|
|
Object Mother with Randomized Test Data
The Object Mother pattern help to have clean and precise code. Utility methods, which are more complex than default builder methods, can be located here. Such methods have a domain background, thus, often they manipulate multiple fields and values at once or in dependence of each other.
This pattern also plays very well with the Builder pattern. The builder methods are fully reused and limit the overhead code to a minimum.
In the following example, I will use the Instancio library to automatically generate randomized test data.
|
|
Example Tests
For this example, a fictional function in a Component under test (cut) is to be tested. This function calculates the total price of an order.
The function accepts an Order
and returns the total price as a BigDecimal
.
In the given step of this test, a Order
must be created with predefined orderItems
and discount
. The remaining fields of the Order
are not
relevant for the test.
Without random test data generation, you either have to set arbitrary values to each field, which will clutter the test, thus, make it harder to understand what data is actually relevant for the test to pass and which is not.
|
|
In the second example, it becomes fairly obvious that the shipping cost depends on the weight, dimensions and country. All other values are random, thus, not relevant.
|
|