Understanding test data patterns

Tests are like a safety net for code and when we write more test suites covering edge cases it helps in increasing the quality and credibility of the code. On the flip side, a clean and readable test is also important as it makes finding bugs and adding new test cases easy. However, over the span of time the test suites grow along with the code and it becomes very hard to read or to add new test cases leading to confusion.

The readability of the test is as important as the readability of the code which is being developed and pushed to production. In this blog, we are going to learn about one of the most common problems which we face while writing a test i.e creation of a class specifically a data class, and how we can handle multiple creations of the data classes cleanly. We will also be covering different patterns/methods of creating data classes cleaner which can be reused along with the pros and cons of each pattern.

Photo by Kelly Sikkema on Unsplash

To start with, we will take the class Person.java. “Person” is a data class or POJO which has an all argument constructor and instance variables that are encapsulated. We will be using the Person Class to write tests and explore different patterns of instantiation of the class. To write our tests we will be using the Junit 5 framework with Java.

Person data class

Individual object creation

The first pattern is a commonly used pattern for writing tests i.e creating an instance of the class inside the test method itself.

Like, we see in the example of PersonTest class where we want to test the validity of a person’s email. To do this, we create a person class instance with a valid email and call isValidEmail() method which returns true if the email is valid and false if the email is invalid.

The test shouldReturnTrueForCorrectEmail() only tests a happy scenario but, we would also want to test one scenario where we want to check what isValidEmail() returns when the email is incorrect. For that, we need to create one more Person class instance with an invalid email and assert the return of isValidEmail() method. If we want to test different variations of invalid email for example email without “@” sign, email without “.” sign. We have to create multiple instances of the Person class with invalid emails.

Person person = new Person(1, “John”, “johngmail.com”, “888888”, “888888”, null, null);

This actually creates a lot of noise in the code, if the data classes or the POJOs are not easy to create and takes a lot of constructor parameters. Since it requires the creation of multiple instances of the same class with the parameter being changed according to the test objective it will result in a lot of redundant and unreadable code.

Ideally, creating a data class in the test gives more clarity if the class getting instantiated in the test is small (taking fewer parameters to create), in this case, we might not see any major problems. But if the POJO class is too big then creating an instance of the classes will be complex and time-consuming.

Test data utilities

Test util for data classes

The next pattern which we are going to explore is a “Test utilities” pattern where we create small test utilities for the creation of the object. We move all the default values inside the utility method so that we have to pass fewer parameters or the parameter we wish to change for the test and the utility handles the creation of the object.

Like in this case, the address can have a default value null as we are testing only the email of the person. So, we can create a utility function which can only take the email or fewer parameter and help in the creation of the object.

In a language like Kotlin where method parameters can have a default value. This utility function can be more enhanced to write something like:

fun aPerson(name: String = “name”, email:String = “email@email.com”) : Person

By doing this we can selectively pass only the field we require to test or change and other variables will use default values as provided.

This really helps a developer in creating instances of POJOs in the test class. It also improves the aesthetics of the code making it appear cleaner and readable. This pattern goes very well with Kotlin but in the case of java, we are still passing multiple parameters, and over a period of time as we want to test more property of the person class we will end up creating multiple utilities or creating the same utility with multiple parameters which again defeats the purpose of creating test utilities.

Test data builder for class

Test builder for data classes

To avoid creating multiple utilities or utilities with multiple parameters. We can explore the Test data builder pattern.

Test data builder like Java builder pattern provides the capability to build the object by setting each property. They can be enhanced to provide a default instance that can be used if we do not want to change any of the property of the class.

For example, in this case, we want to build a person with an invalid email to test the validity of the email. Then for the Person class, we just change the value of email. The builder provides a default implementation for the Person class and any property which we want to change can be done in the specific test. For example, we want to set the property email in this test. We can do it in the following way:

PersonBuilder.defaultBuilder(). email(“99999”).build()

This returns a person with an invalid email. It makes the creation of the test data much simpler and easier. Also helps in keeping the test clean and easy to understand, as for each test we can just change one value and test only that value which eventually results in modular and smaller level tests.

However, with all the pros this pattern has, it also comes with an overhead of creating a builder in the test code which can be a lengthy task to create and maintain. As we can see in the example, test builders can be very big depending on the number of instance variables the POJO will have. Also, if there are a lot of POJOs in the project we can end up creating a lot of builders for it.

So, do we need a builder for every class in the project? The answer will be no. we need to be very careful before creating a builder for every POJO or model, we can choose to create a builder for only those classes which have a lot of instance variables. For small classes, in-place creation of the object in the test class makes the code more readable and helps us reducing the overhead of creating a builder for it.

Lombok test data builders

Lombok test builder

Lombok is a code generation library that allows a developer to create getter, setter, constructor, and utilities over a class without worrying about writing and maintaining the code for that class.

We can use this property of Lombok to actually create builders for our tests more easily. It will help us to keep the test clean and easy to maintain in a long run.

Like in the example, PersonTestData class extends Person and the @Builder annotation can be used to create a builder of type PersonTestData, this can be used anywhere we need Person class as it is a child class of Person. By, using this pattern we do not have to worry about writing code to construct the builder but it can be all autogenerated by the Lombok annotation.

This helps us to write clean and more meaningful tests. A scenario where this approach might not be suitable is when reflection is used. As the class returned by the builder is PersonTestData class and not the Person class. So, whenever we are using this pattern we can not use it with spring entity, or java reflections.

All these patterns are beneficial in their own ways, so to use one over the other is a judgment call that needs to be evaluated and then taken before writing the test. If the class is small and easy to create maybe the “Individual object creation” pattern will do the job. If there are a lot of variables to change and test “Lombok builders” can help to write better tests. The end goal is to write clean and maintainable tests along with clean code.

Thanks !!

Love Coding, Algorithm, Problem-solving, learning new technology :) Application Developer at ThoughtWorks