Creating Our First Test Data Builder
After we have finished this lesson, we:
- Know what a test data builder is.
- Can create a test data builder that creates data container objects.
- Can identify the benefits of using a test data builder.
- Understand when we should use test data builders for creating data container objects.
This lesson assumes that:
- You are familiar with the system under test
- You can create test data by using the
newkeyword (the telescoping constructor antipattern and the JavaBeans style construction ) - You are familiar with factory methods
- You can create test data by using factory methods (local factory methods and an object mother class )
Get the source code from Github
What Is a Test Data Builder?
A test data builder is a class that uses the builder pattern for creating objects which are used by our test methods. We can use test data builders for solving the problems caused by the new keyword and factory methods.
To be more specific, when we create a new test data builder, we want to meet these goals:
First, we want to provide a “foolproof” API for creating our test data. If we create our test data by using either the new keyword or factory methods, we might have to create our test data by using a “slow API”.
For example, if we want to create U.S. addresses which have a street address by using the new keyword and a factory method, the code that creates these addresses could look as follows:
1MailingAddress usAddress = new MailingAddress("Teddy Tester",
2 "Test Street 51",
3 null,
4 "11111",
5 "Test City",
6 "TX",
7 "USA"
8);
9
10MailingAddress usAddress = MailingAddressFactory
11 .createNormalUSAddress("Teddy Tester",
12 "Test Street 51",
13 "11111",
14 "Test City",
15 "TX"
16 );
The problem is that creating our test data is quite slow because we have to remember to pass the constructor arguments or method parameters in the correct order. This is tricky because we have to pass “too many” constructor arguments or method parameters. Also, the order of these arguments or parameters is logical only if the developer is familiar with the Finnish address format.
Second, we want to make it easy to identify the property values of the created objects. If we want to write assertions for the created objects, the property values of these objects should be clear to anyone who reads our test code.
Again, if we want to create U.S. addresses which have a street address by using the new keyword and a factory method, the code that creates these addresses could look as follows:
1MailingAddress usAddress = new MailingAddress("Teddy Tester",
2 "Test Street 51",
3 null,
4 "11111",
5 "Test City",
6 "TX",
7 "USA"
8);
9
10MailingAddress usAddress = MailingAddressFactory
11 .createNormalUSAddress("Teddy Tester",
12 "Test Street 51",
13 "11111",
14 "Test City",
15 "TX"
16 );
The problem is that if we want to figure out the field values of the created objects, we might have to read the source code of the constructor or factory method. Typically, the reason for this is that at least one of the following conditions is true:
- Our test code uses values that make no sense. For example, if we want to set the street address of a
MailingAddressobject, we shouldn’t use the value: ‘FooBar’. - The constructor has too many constructor arguments and the subsequent constructor arguments have the same type.
- The factory method has too many method parameters and the subsequent method parameters have the same type.
- The order of constructor arguments or method parameters is logical only to people who belong to a specific group. In this case, the developers must be familiar with the Finnish address format.
Third, we want to provide an easy way to create test data that has a lot of variation. The object mother pattern (and factory methods in general) isn’t good at dealing with variation found from our test data. Typically, when we have to create slightly different test data, we simply add new methods to our object mother class.
For example, if we have to create different Task objects, we could use the following factory methods:
1Task openTaskWithoutAssignee = TaskFactory
2 .createOpenTaskWithoutAssignee();
3
4Task openTaskWithAssignee = TaskFactory
5 .createOpenTaskThatIsAssignedToAssignee();
6
7Task taskThatWasClosedAsDuplicate = TaskFactory
8 .createTaskThatWasClosedAsDuplicate();
The problem is that if we continue adding new methods to our object mother class, it becomes a messy class that is hard to maintain.
If we don’t refactor it, our object mother class is full of methods which create slightly different Task objects. The problem is that these methods have a lot of duplicate code. For example, our object mother class could look as follows:
1public class TaskFactory {
2
3 public static Task createOpenTaskWithoutAssignee() {
4 //Creates a new Task object
5 }
6
7 public static Task createOpenTaskThatIsAssignedToAssignee() {
8 //Creates a new Task object
9 }
10
11 public static Task createTaskThatWasClosedAsDuplicate() {
12 //Creates a new Task object
13 }
14}
On the other hand, if we refactor our mother class, it has many methods that simply call the private method which creates the required object. This is a problem because the changes made to the private method can cause unwanted side effects.
After we have written this private method, our object mother class could look as follows:
1public class TaskFactory {
2
3 public static Task createOpenTaskWithoutAssignee() {
4 return createTask(
5 //Pass method parameters
6 )
7 }
8
9 public static Task createOpenTaskThatIsAssignedToAssignee() {
10 return createTask(
11 //Pass method parameters
12 )
13 }
14
15 public static Task createTaskThatWasClosedAsDuplicate() {
16 return createTask(
17 //Pass method parameters
18 )
19 }
20
21 private static Task createTask(Long taskId,
22 Long assigneeId,
23 Long creatorId,
24 Long modifierId,
25 Long closerId,
26 String title,
27 String description,
28 TaskStatus status,
29 TaskResolution resolution) {
30 //Creates a new Task object
31 }
32}
Fourth, we want to move our object creation logic (aka business rules) from our test classes to a test data builder because we want to remove duplicate code from our test suite. If we remove duplicate code from our test suite, we can enjoy from these three benefits:
- If the created objects have different states, our test methods are easier to read because the person who reads our test methods doesn’t have to be familiar with the implementation of these states.
- It’s easier to write tests which use the created objects because we don’t have to implement our business rules in our test methods. In other words, we can create our test data by writing less code.
- If we make changes to our business rules, we have to make these changes only to our test data builder. This makes our test suite easier to maintain.
Additional Reading:
Next, we will create our first test data builder.
Creating Our First Test Data Builder
Let’s create a test data builder that creates new MailingAddress objects. We can create our test data builder by following these steps:
First, we have to create a test data builder class by following these steps:
- Create a
publicandfinalclass. We have to mark the created class asfinalbecause we don’t want that other developers are able to extend our test data builder class. - Add a no-argument constructor to the created class.
After we have created our test data builder class, its source code looks as follows:
1public final class MailingAddressBuilder {
2
3 public MailingAddressBuilder() {
4
5 }
6}
There are two things I want to point out:
- When I name my test data builder classes, I append the string: ‘Builder’ to the type of the created object. Because our test data builder class creates
MailingAddressobjects, its name should be:MailingAddressBuilder. - This example assumes that we want to use our test data builder from multiple classes which aren’t necessarily in the same package as our test data builder class. However, if this is not the case, we must limit the visibility of our test data builder class.
Second, we have to add new fields to our test data builder class. These fields contain the field values of the created MailingAddress object.
After we added these fields to our test data builder class, its source code looks as follows:
1public final class MailingAddressBuilder {
2
3 private String receiver;
4 private String streetAddress;
5 private String postOfficeBox;
6 private String postCode;
7 private String city;
8 private String state;
9 private String country;
10
11 public MailingAddressBuilder() {
12
13 }
14}
Third, we have to write the methods which set the field values of our test data builder class. Because we want to create a fluent API, we have to write these methods by following these steps:
- Add a
publicmethod to the test data builder class. This method takes the field value as a method parameter and returns a reference to our test data builder object. - Implement the method by setting the field value. Remember to return a reference to our test data builder object.
After we have added these methods to our test data builder class, its source code looks as follows:
1public final class MailingAddressBuilder {
2
3 private String receiver;
4 private String streetAddress;
5 private String postOfficeBox;
6 private String postCode;
7 private String city;
8 private String state;
9 private String country;
10
11 public MailingAddressBuilder() {
12
13 }
14
15 public MailingAddressBuilder withReceiver(String receiver) {
16 this.receiver = receiver;
17 return this;
18 }
19
20 public MailingAddressBuilder withStreetAddress(String streetAddress) {
21 this.streetAddress = streetAddress;
22 return this;
23 }
24
25 public MailingAddressBuilder withPostOfficeBox(String postOfficeBox) {
26 this.postOfficeBox = postOfficeBox;
27 return this;
28 }
29
30 public MailingAddressBuilder withPostCode(String postCode) {
31 this.postCode = postCode;
32 return this;
33 }
34
35 public MailingAddressBuilder withCity(String city) {
36 this.city = city;
37 return this;
38 }
39
40 public MailingAddressBuilder withState(String state) {
41 this.state = state;
42 return this;
43 }
44
45 public MailingAddressBuilder withCountry(String country) {
46 this.country = country;
47 return this;
48 }
49}
When we name these methods, we can follow one of these two rules:
- We can use the name of the field as a method name:
state(String state). - We can prepend the string ‘with’ to the name of the field:
withState(String state).
Even though I follow the second rule, you are free to make your own choices as long as you are consistent.
Fourth, we have to write the method that creates a new MailingAddress object. This method is called: build(), and it has no method parameters.
After we have added this method to our test data builder class, its source code looks as follows:
1public final class MailingAddressBuilder {
2
3 private String receiver;
4 private String streetAddress;
5 private String postOfficeBox;
6 private String postCode;
7 private String city;
8 private String state;
9 private String country;
10
11 public MailingAddressBuilder() {
12
13 }
14
15 public MailingAddressBuilder withReceiver(String receiver) {
16 this.receiver = receiver;
17 return this;
18 }
19
20 public MailingAddressBuilder withStreetAddress(String streetAddress) {
21 this.streetAddress = streetAddress;
22 return this;
23 }
24
25 public MailingAddressBuilder withPostOfficeBox(String postOfficeBox) {
26 this.postOfficeBox = postOfficeBox;
27 return this;
28 }
29
30 public MailingAddressBuilder withPostCode(String postCode) {
31 this.postCode = postCode;
32 return this;
33 }
34
35 public MailingAddressBuilder withCity(String city) {
36 this.city = city;
37 return this;
38 }
39
40 public MailingAddressBuilder withState(String state) {
41 this.state = state;
42 return this;
43 }
44
45 public MailingAddressBuilder withCountry(String country) {
46 this.country = country;
47 return this;
48 }
49
50 public MailingAddress build() {
51 return new MailingAddress(receiver,
52 streetAddress,
53 postOfficeBox,
54 postCode,
55 city,
56 state,
57 country
58 );
59 }
60}
Additional Reading:
We have now created a simple test data builder class. Let’s move on and find out how we can create new MailingAddress objects by using our test data builder.
Creating Test Data With a Test Data Builder
Before we can create test data by using our new test data builder, we have to create a new test class and add all required constants to our test class.
After we have created our test class, its source code looks as follows:
1public class MailingAddressTest {
2
3 private static final String CITY = "Test City";
4 private static final String COUNTRY_UNITED_STATES = "United States";
5 private static final String POST_CODE = "11111";
6 private static final String POST_OFFICE_BOX = "PO Box 1";
7 private static final String RECEIVER = "Teddy Tester";
8 private static final String STATE = "TX";
9 private static final String STREET_ADDRESS = "Test Street 51";
10
11}
Let’s create two MailingAddress objects which contain U.S. addresses:
- Create an address that has a street address.
- Create an address that has a post office box.
After we have written the test methods which create these MailingAddress objects, the source code of our test class looks as follows:
1mport org.junit.Test;
2
3public class MailingAddressTest {
4
5 private static final String CITY = "Test City";
6 private static final String COUNTRY_UNITED_STATES = "United States";
7 private static final String POST_CODE = "11111";
8 private static final String POST_OFFICE_BOX = "PO Box 1";
9 private static final String RECEIVER = "Teddy Tester";
10 private static final String STATE = "TX";
11 private static final String STREET_ADDRESS = "Test Street 51";
12
13 @Test
14 public void createUSAddress() {
15 MailingAddress usAddress = new MailingAddressBuilder()
16 .withReceiver(RECEIVER)
17 .withStreetAddress(STREET_ADDRESS)
18 .withPostCode(POST_CODE)
19 .withCity(CITY)
20 .withState(STATE)
21 .withCountry(COUNTRY_UNITED_STATES)
22 .build();
23 }
24
25 @Test
26 public void createUSPoBoxAddress() {
27 MailingAddress usPoBoxAddress = new MailingAddressBuilder()
28 .withReceiver(RECEIVER)
29 .withPostOfficeBox(POST_OFFICE_BOX)
30 .withPostCode(POST_CODE)
31 .withCity(CITY)
32 .withState(STATE)
33 .withCountry(COUNTRY_UNITED_STATES)
34 .build();
35 }
36}
We have now created two MailingAddress objects. Next, we will compare our test code with code samples which use the new keyword and a factory method. However, before we can analyze our code any further, we have to take a look at two code samples which create a U.S. address that has a street address. These code samples look as follows:
1MailingAddress usAddress = new MailingAddress("Teddy Tester",
2 "Test Street 51",
3 null,
4 "11111",
5 "Test City",
6 "TX",
7 "USA"
8);
9
10MailingAddress usAddress = MailingAddressFactory
11 .createNormalUSAddress("Teddy Tester",
12 "Test Street 51",
13 "11111",
14 "Test City",
15 "TX"
16 );
When we compare our code samples with each other, we notice that a test data builder solves the problems caused by the new keyword and a factory method. Also, we notice that using a test data builder has the following benefits:
- The method names of our test data builder class identify the field values of the created object. This makes our code faster to write because we don’t have to remember the order of constructor arguments or method parameters. On the other hand, this makes our code faster to read because we can see the field values of the created object by simply reading the source code of our test method.
- A test data builder provides first-class support for creating test data that has a lot of variation. If we want to create objects that contain different data, we don’t have to make any changes to our test data builder class. We can create our test data by simply calling the relevant methods. For example, we can use the same test data builder for creating Finnish, Swedish, and U.S. addresses.
Next, we will find out when we should use test data builders for creating data container objects.
Should We Use a Test Data Builder?
When we decide if we should create data container objects with a test data builder, we have to decide if the benefits of using a test data builder are greater than the cost of creating an extra class that must be maintained in the future. I think that we can make this decision by following these rules:
- If the data container class uses the telescoping constructor (anti)pattern, we should create our test data by using a test data builder. This is a good idea because a test data builder identifies the field values of the created object and provides a good support for test data that has a lot of variation. In other words, a test data builder solves the problems caused by the
newkeyword and factory methods, and helps us to write tests which are easy to read, write, and maintain. - If the data container class uses the JavaBeans style construction, we shouldn’t use a test data builder because the setter methods identify the field values of the created object and provide a good support for test data that has a lot of variation. In other words, a test data builder has no benefits over the JavaBeans style construction.
- If the data container class is used by only one test class or a few test classes, we shouldn’t use a test data builder even if the data container class uses the telescoping constructor (anti)pattern. If we are in this situation, the cost of creating a test data builder is most likely higher than its benefits.
We can now create data container objects with a test data builder, and we know when we should use this technique in our test classes. Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us six things:
- A test data builder identifies the field values of the created object.
- A test data builder provides a good support for test data that has a lot of variation.
- The downside of using a test data builder is that we have to create an extra class that must be maintained in the future.
- We should create data container objects with a test data builder if the data container class uses the telescoping constructor (anti)pattern.
- We shouldn’t create data container objects with a test data builder if the data container class uses the JavaBeans style construction.
- We shouldn’t use a test data builder if the data container class is used only by one test class or a few test classes.