Overriding a Builder With a Test Data Builder

After we have finished this lesson, we

  • Know why we should override a builder with a test data builder.
  • Understand when we should override a builder with our own test data builder.
  • Can create a test data builder that overrides a builder.

Get the source code from Github

The Perils of the Builder Pattern

Even though the builder pattern is a great tool for creating stateful objects, it can cause us a lot of problems when we are writing tests which create new objects by using the provided builder.

The root cause of these problems is that the builder pattern helps us to make our production code safe and easy to read and write. When we implement the builder pattern, we want to fulfill these three goals:

  • We want to solve the problems caused by the telescoping constructor (anti)pattern.
  • We want to provide an API which allows us to create an object that’s in its initial state.
  • We want to ensure that we cannot create an object that’s in an invalid state.

These are excellent goals. However, when we fulfill these goals, our choices have a huge effect on our test code. For example, let’s assume that the Task class provides a builder which ensures that we can create only open tasks. The source code of the Task class looks as follows:

 1public class Task {
 2
 3    private Long id;
 4    private Assignee assignee;
 5    private Closer closer;
 6    private Creator creator;
 7    private String description;
 8    private Modifier modifier;
 9    private String title;
10    private TaskStatus status;
11    private TaskResolution resolution;
12
13    private Task(Builder builder) {
14        this.id = builder.id;
15        this.assignee = builder.assignee;
16        this.closer = null;
17        this.creator = builder.creator;
18        this.description = builder.description;
19        this.modifier = builder.modifier;
20        this.title = builder.title;
21        this.status = TaskStatus.OPEN;
22        this.resolution = null;
23    }
24
25    public static Builder getBuilder() {
26        return new Builder();
27    }
28
29    //Getters are omitted
30
31    public static class Builder {
32
33        private Long id;
34        private Assignee assignee;
35        private Creator creator;
36        private String description;
37        private Modifier modifier;
38        private String title;
39
40        private Builder() {}
41
42        public Builder withId(Long id) {
43            this.id = id;
44            return this;
45        }
46
47        public Builder withAssignee(Long assigneeId) {
48            if (assigneeId != null) {
49                this.assignee = new Assignee(assigneeId);
50            }
51            return this;
52        }
53
54        public Builder withCreator(Long creatorId) {
55            if (creatorId != null) {
56                this.creator = new Creator(creatorId);
57                this.modifier = new Modifier(creatorId);
58            }
59            return this;
60        }
61
62        public Builder withDescription(String description) {
63            this.description = description;
64            return this;
65        }
66
67        public Builder withTitle(String title) {
68            this.title = title;
69            return this;
70        }
71
72        public Task build () {
73            return new Task(this);
74        }
75    }
76}

When we take a look at this class we notice that the builder is useless to us because:

  • If we want to create a task that’s in progress, we have to figure out a way to change the status of created Task object.
  • If we want to create a closed task, we have to figure out a way to set the closer of the created task, and change the status and resolution of the task.

In other words, the goals of the builder pattern are incompatible with the requirements of our test code. The builder pattern protects us from our own mistakes, and this is a good thing when we are writing production code. However, when we are writing test code, we want to use a clean and flexible API that helps us to save time when we create our test data.

We can try to solve this problem by using one of these three options:

First, we can add new methods to our test classes and set the required field values by using these methods. If we use this option, we put the object creation logic (aka business logic) to our test classes. As we remember, this is a problem because it makes our tests hard to read, write, and maintain.

Second, we can add new methods to the Task class and set the required field values by using these methods. If we use this option, we end up writing production code that fulfills the requirements of our test code. The problem is that the requirements of our test code are often incompatible with the requirements of our production code.

As we remember, our production code must allow us to create an object that’s in its initial state and ensure that we cannot create an object that’s in an invalid state. That’s why our implementation of the builder pattern allows us to create only open tasks.

On the other hand, if we want to write comprehensive tests for our application, we have to be able to create Task objects which are in different states.

Third, if the Task class already has public methods which change the state of a task, we can use these methods. However, we must remember that the goal of these methods is to ensure that the user of our application cannot perform an illegal state transition. For example, it’s possible that we can close only tasks which are in progress.

Generally speaking, the problem is that the rules concerning the state transitions of a stateful object are most likely incompatible with the requirements of our test code. For example, it’s possible (and even likely) that we cannot trigger a state transition from the initial state of an object to its final state. Instead, we have to trigger multiple state transitions. This means that if we use this option, our object creation code will be more complex than it could be.

Next, I will introduce a clean solution that doesn’t force us to add test code to our production codebase. To be more specific, we will find out how we can write a test data builder that solves our problem.

Creating a Test Data Builder That Overrides the Builder Pattern

Our goal is to create a test data builder that has the same API as the test data builder which we created in the previous lesson . That’s why we won’t create our new test data builder from the scratch. Instead, we will make the following changes to the test data builder which we created in the previous lesson:

First, we have to ensure that our test data builder is compatible with the API of our builder. We can do this by following these steps:

  1. Delete the assignee, closer, creator, and modifier fields.
  2. Add the assigneeId, closerId, creatorId, and modifierId fields to our test data builder class.
  3. Modify the methods which set the field values of the assignee, closer, creator, and modifier fields. The new implementations of these methods set the values of the assigneeId, closerId, creatorId, and modifierId fields.
  4. Ensure that the build() method creates a new Task object by using the builder that’s provided by the Task class.

After we have made these changes, the source code of our test data builder class looks as follows:

 1public final class TaskBuilder {
 2
 3    private Long assigneeId;
 4    private Long closerId;
 5    private Long creatorId;
 6    private String description;
 7    private Long id;
 8    private Long modifierId;
 9    private TaskResolution resolution;
10    private TaskStatus status;
11    private String title;
12
13    public TaskBuilder() {
14
15    }
16
17    public TaskBuilder withAssignee(Long assigneeId) {
18        this.assigneeId = assigneeId;
19        return this;
20    }
21
22    public TaskBuilder withCreator(Long creatorId) {
23        this.creatorId = creatorId;
24        return this;
25    }
26
27    public TaskBuilder withDescription(String description) {
28        this.description = description;
29        return this;
30    }
31
32    public TaskBuilder withId(Long id) {
33        this.id = id;
34        return this;
35    }
36
37    public TaskBuilder withModifier(Long modifierId) {
38        this.modifierId = modifierId;
39        return this;
40    }
41
42    public TaskBuilder withResolutionDone(Long closerId) {
43        this.closerId = closerId;
44        this.resolution = TaskResolution.DONE;
45        this.status = TaskStatus.CLOSED;
46        return this;
47    }
48
49    public TaskBuilder withResolutionDuplicate(Long closerId) {
50        this.closerId = closerId;
51        this.resolution = TaskResolution.DUPLICATE;
52        this.status = TaskStatus.CLOSED;
53        return this;
54    }
55
56    public TaskBuilder withResolutionWontDo(Long closerId) {
57        this.closerId = closerId;
58        this.resolution = TaskResolution.WONT_DO;
59        this.status = TaskStatus.CLOSED;
60        return this;
61    }
62
63    public TaskBuilder withStatusInProgress() {
64        this.closerId = null;
65        this.resolution = null;
66        this.status = TaskStatus.IN_PROGRESS;
67        return this;
68    }
69
70    public TaskBuilder withStatusOpen() {
71        this.closerId = null;
72        this.resolution = null;
73        this.status = TaskStatus.OPEN;
74        return this;
75    }
76
77    public TaskBuilder withTitle(String title) {
78        this.title = title;
79        return this;
80    }
81
82    public Task build() {
83        return Task.getBuilder()
84                .withAssignee(assigneeId)
85                .withCreator(creatorId)
86                .withDescription(description)
87                .withId(id)
88                .withTitle(title)
89                .build();
90    }
91}

Second, we have to add a private setFieldValue() method to the TaskBuilder class. This method sets a field value of a Task object by using reflection, and it has three method parameters:

  • The Task object whose field value is set.
  • The name of the field whose value is set.
  • The new field value.

After we have added this method to the TaskBuilder class, its source code looks as follows:

  1import java.lang.reflect.Field;
  2
  3public final class TaskBuilder {
  4
  5    private Long assigneeId;
  6    private Long closerId;
  7    private Long creatorId;
  8    private String description;
  9    private Long id;
 10    private Long modifierId;
 11    private TaskResolution resolution;
 12    private TaskStatus status;
 13    private String title;
 14
 15    public TaskBuilder() {
 16
 17    }
 18
 19    public TaskBuilder withAssignee(Long assigneeId) {
 20        this.assigneeId = assigneeId;
 21        return this;
 22    }
 23
 24    public TaskBuilder withCreator(Long creatorId) {
 25        this.creatorId = creatorId;
 26        return this;
 27    }
 28
 29    public TaskBuilder withDescription(String description) {
 30        this.description = description;
 31        return this;
 32    }
 33
 34    public TaskBuilder withId(Long id) {
 35        this.id = id;
 36        return this;
 37    }
 38
 39    public TaskBuilder withModifier(Long modifierId) {
 40        this.modifierId = modifierId;
 41        return this;
 42    }
 43
 44    public TaskBuilder withResolutionDone(Long closerId) {
 45        this.closerId = closerId;
 46        this.resolution = TaskResolution.DONE;
 47        this.status = TaskStatus.CLOSED;
 48        return this;
 49    }
 50
 51    public TaskBuilder withResolutionDuplicate(Long closerId) {
 52        this.closerId = closerId;
 53        this.resolution = TaskResolution.DUPLICATE;
 54        this.status = TaskStatus.CLOSED;
 55        return this;
 56    }
 57
 58    public TaskBuilder withResolutionWontDo(Long closerId) {
 59        this.closerId = closerId;
 60        this.resolution = TaskResolution.WONT_DO;
 61        this.status = TaskStatus.CLOSED;
 62        return this;
 63    }
 64
 65    public TaskBuilder withStatusInProgress() {
 66        this.closerId = null;
 67        this.resolution = null;
 68        this.status = TaskStatus.IN_PROGRESS;
 69        return this;
 70    }
 71
 72    public TaskBuilder withStatusOpen() {
 73        this.closerId = null;
 74        this.resolution = null;
 75        this.status = TaskStatus.OPEN;
 76        return this;
 77    }
 78
 79    public TaskBuilder withTitle(String title) {
 80        this.title = title;
 81        return this;
 82    }
 83
 84    public Task build() {
 85        return Task.getBuilder()
 86                .withAssignee(assigneeId)
 87                .withCreator(creatorId)
 88                .withDescription(description)
 89                .withId(id)
 90                .withTitle(title)
 91                .build();
 92    }
 93
 94    private void setFieldValue(Task target,
 95                               String fieldName,
 96                               Object fieldValue) {
 97        try {
 98            Field targetField = target.getClass()
 99                    .getDeclaredField(fieldName);
100
101            targetField.setAccessible(true);
102            targetField.set(target, fieldValue);
103        }
104        catch (Exception ex) {
105            throw new RuntimeException(
106                    String.format(
107                            "Cannot set the value of the field: %s",
108                            fieldName
109                    ),
110                    ex
111            );
112        }
113    }
114}

Third, we have to make the following changes to the build() method of our test data builder class:

  1. Store the created Task object in a local variable.
  2. Set the field values of the closer, modifier, status, and resolution fields by using reflection.
  3. Return the created object.

After we have made the required changes to the build() method, the source code of our test data builder looks as follows:

  1import java.lang.reflect.Field;
  2
  3public final class TaskBuilder {
  4
  5    private Long assigneeId;
  6    private Long closerId;
  7    private Long creatorId;
  8    private String description;
  9    private Long id;
 10    private Long modifierId;
 11    private TaskResolution resolution;
 12    private TaskStatus status;
 13    private String title;
 14
 15    public TaskBuilder() {
 16
 17    }
 18
 19    public TaskBuilder withAssignee(Long assigneeId) {
 20        this.assigneeId = assigneeId;
 21        return this;
 22    }
 23
 24    public TaskBuilder withCreator(Long creatorId) {
 25        this.creatorId = creatorId;
 26        return this;
 27    }
 28
 29    public TaskBuilder withDescription(String description) {
 30        this.description = description;
 31        return this;
 32    }
 33
 34    public TaskBuilder withId(Long id) {
 35        this.id = id;
 36        return this;
 37    }
 38
 39    public TaskBuilder withModifier(Long modifierId) {
 40        this.modifierId = modifierId;
 41        return this;
 42    }
 43
 44    public TaskBuilder withResolutionDone(Long closerId) {
 45        this.closerId = closerId;
 46        this.resolution = TaskResolution.DONE;
 47        this.status = TaskStatus.CLOSED;
 48        return this;
 49    }
 50
 51    public TaskBuilder withResolutionDuplicate(Long closerId) {
 52        this.closerId = closerId;
 53        this.resolution = TaskResolution.DUPLICATE;
 54        this.status = TaskStatus.CLOSED;
 55        return this;
 56    }
 57
 58    public TaskBuilder withResolutionWontDo(Long closerId) {
 59        this.closerId = closerId;
 60        this.resolution = TaskResolution.WONT_DO;
 61        this.status = TaskStatus.CLOSED;
 62        return this;
 63    }
 64
 65    public TaskBuilder withStatusInProgress() {
 66        this.closerId = null;
 67        this.resolution = null;
 68        this.status = TaskStatus.IN_PROGRESS;
 69        return this;
 70    }
 71
 72    public TaskBuilder withStatusOpen() {
 73        this.closerId = null;
 74        this.resolution = null;
 75        this.status = TaskStatus.OPEN;
 76        return this;
 77    }
 78
 79    public TaskBuilder withTitle(String title) {
 80        this.title = title;
 81        return this;
 82    }
 83
 84    public Task build() {
 85        Task task = Task.getBuilder()
 86                .withAssignee(assigneeId)
 87                .withCreator(creatorId)
 88                .withDescription(description)
 89                .withId(id)
 90                .withTitle(title)
 91                .build();
 92
 93        if (closerId != null) {
 94            setFieldValue(task, "closer", new Closer(closerId));
 95        }
 96
 97        if (modifierId != null) {
 98            setFieldValue(task, "modifier", new Modifier(modifierId));
 99        }
100
101        setFieldValue(task, "status", status);
102        setFieldValue(task, "resolution", resolution);
103
104        return task;
105    }
106
107    private void setFieldValue(Task target,
108                               String fieldName,
109                               Object fieldValue) {
110        try {
111            Field targetField = target.getClass()
112                    .getDeclaredField(fieldName);
113
114            targetField.setAccessible(true);
115            targetField.set(target, fieldValue);
116        }
117        catch (Exception ex) {
118            throw new RuntimeException(
119                    String.format(
120                            "Cannot set the value of the field: %s",
121                            fieldName
122                    ),
123                    ex
124            );
125        }
126    }
127}

We have now created a test data builder which overrides the builder that’s provided by the Task class. Because we didn’t make any changes to the API of our test data builder, we don’t have to make any changes to our existing unit tests. For example, if we want to create a task that was closed as a duplicate, we can still use the following code:

 1import org.junit.Test;
 2
 3public class TaskTest {
 4
 5    private static final Long ID = 1L;
 6    private static final Long ASSIGNEE_ID = 99L;
 7    private static final Long CLOSER_ID = 55L;
 8    private static final Long CREATOR_ID = 44L;
 9    private static final Long MODIFIER_ID = 66L;
10    private static final String TITLE = "Test data builder example";
11    private static final String DESCRIPTION = "Write an example project";
12
13    @Test
14    public void createTaskThatWasClosedAsDuplicate() {
15        Task duplicate = new TaskBuilder()
16                .withId(ID)
17                .withAssignee(ASSIGNEE_ID)
18                .withCreator(CREATOR_ID)
19                .withModifier(MODIFIER_ID)
20                .withTitle(TITLE)
21                .withDescription(DESCRIPTION)
22                .withResolutionDuplicate(CLOSER_ID)
23                .build();
24    }
25}

Let’s move on and take a look at the pros and cons of our new test data builder.

The Pros and Cons of Our New Test Data Builder

The pros of our test data builder are:

  • We can hide the object creation logic (aka business logic) from the people who read and write our tests. This means that our actual test code is easy to read and write.
  • We can put the object creation logic (aka business logic) to one place. This ensures that our tests are easy to write and maintain.
  • We don’t have to make any changes to our production code. In other words, if we use a test data builder, we can keep our production code clean and as easy to read as possible.

The cons of our test data builder are:

  • Our test data builder uses reflection. This can cause problems if we rename the fields of the Task class and we don’t use an IDE. However, this isn’t a big problem because most of the time we are using an IDE.
  • Using reflection is more complex than adding new methods to the Task class. However, this isn’t a huge problem because we have to write this code only once.
  • We have to specify the field values which are required by our builder. This can be cumbersome if the created object has many mandatory fields. We will talk more about this problem in the next lesson of this topic.

Next, we will find out when we should override a builder with a test data builder.

When Should We Use This Technique?

As we remember, the previous lesson of this topic taught us that we should create stateful objects with a test data builder if these objects are used by multiple test classes, and we cannot use a factory method or the new keyword. However, we must remember that our test data builder created our test data by using the JavaBeans style construction.

During this lesson we created a test data builder that overrides the builder provided by the Task class. Also, this lesson demonstrated that this technique helps us to write tests which are easy to read, write, and maintain. Even though this technique has minor drawbacks, it should be clear that its pros outweigh the cons.

That’s why I think that we should override a builder with a test data builder if we are creating stateful objects which are used by multiple test classes, and we cannot use a factory method.

We know why we should override a builder with a test data builder, we can write a test data builder which overrides a builder, and we understand when we should use this technique. Let’s summarize what we learned from this lesson.

Summary

This lesson has taught us three things:

  • The goals of the builder pattern are incompatible with the requirements of our test code.
  • We should override a builder with a test data builder if we are creating stateful objects which are used by multiple test classes, and we cannot use a factory method.
  • When we override a builder with a test data builder, we shouldn’t make any changes to our production code. Instead, we should use reflection.

Get the source code from Github