Creating Stateful Objects With Test Data Builders

After we have finished this lesson, we:

  • Understand why we shouldn’t create stateful objects with a data-centric test data builder.
  • Can create test data builders which hide our business logic behind a fluent API.
  • Know when we should create stateful objects with a test data builder.

Get the source code from Github

The Cons of Data-Centric Test Data Builders

As we remember , the Task class uses the JavaBeans style construction, and a Task object can have multiple statuses and resolutions. In other words, a Task object isn’t just a data container. It has an internal state that makes our life hard if we create our test data by using the new keyword or factory methods.

However, using a test data builder doesn’t necessarily make our life any easier. For example, if we create our test data builder by following the instructions given in the previous lesson of this topic , the source code of our test data builder looks as follows:

 1public final class TaskBuilder {
 2
 3    //Fields
 4    private Assignee assignee;
 5    private Closer closer;
 6    private Creator creator;
 7    private String description;
 8    private Long id;
 9    private Modifier modifier;
10    private TaskResolution resolution;
11    private TaskStatus status;
12    private String title;
13
14    public TaskBuilder() {
15
16    }
17
18    //Methods which set the field values
19    public TaskBuilder withAssignee(Long assigneeId) {
20        this.assignee = new Assignee(assigneeId);
21        return this;
22    }
23
24    public TaskBuilder withCloser(Long closerId) {
25        this.closer = new Closer(closerId);
26        return this;
27    }
28
29    public TaskBuilder withCreator(Long creatorId) {
30        this.creator = new Creator(creatorId);
31        return this;
32    }
33
34    public TaskBuilder withDescription(String description) {
35        this.description = description;
36        return this;
37    }
38
39    public TaskBuilder withId(Long id) {
40        this.id = id;
41        return this;
42    }
43
44    public TaskBuilder withModifier(Long modifierId) {
45        this.modifier = new Modifier(modifierId);
46        return this;
47    }
48
49    public TaskBuilder withResolution(TaskResolution resolution) {
50        this.resolution = resolution;
51        return this;
52    }
53
54    public TaskBuilder withStatus(TaskStatus status) {
55        this.status = status;
56        return this;
57    }
58
59    public TaskBuilder withTitle(String title) {
60        this.title = title;
61        return this;
62    }
63
64    //A method that creates a new Task object, sets
65    //the field values of the created object, and
66    //returns the created object.
67    public Task build() {
68        Task task = new Task();
69
70        task.setAssignee(assignee);
71        task.setCloser(closer);
72        task.setCreator(creator);
73        task.setDescription(description);
74        task.setId(id);
75        task.setModifier(modifier);
76        task.setResolution(resolution);
77        task.setStatus(status);
78        task.setTitle(title);
79
80        return task;
81    }
82}

Let’s demonstrate the problems caused by this test data builder. Next, we will write two test methods which create new Task objects which were closed as a duplicate. These methods are:

  • The buildTaskThatWasClosedAsDuplicate() method uses our new test data builder.
  • The createTaskThatWasClosedAsDuplicate() method uses the new keyword

After we have added these methods to our test class, its source code looks as follows:

 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 = "A test data builder example";
11    private static final String DESCRIPTION = "Write an example project";
12
13    @Test
14    public void buildTaskThatWasClosedAsDuplicate() {
15        Task duplicate = new TaskBuilder()
16                .withId(ID)
17                .withAssignee(ASSIGNEE_ID)
18                .withCloser(CLOSER_ID)
19                .withCreator(CREATOR_ID)
20                .withModifier(MODIFIER_ID)
21                .withTitle(TITLE)
22                .withDescription(DESCRIPTION)
23                .withStatus(TaskStatus.CLOSED)
24                .withResolution(TaskResolution.DUPLICATE)
25                .build();
26    }
27
28    @Test
29    public void createTaskThatWasClosedAsDuplicate() {
30        Task duplicate = new Task();
31        duplicate.setId(ID);
32        duplicate.setAssignee(new Assignee(ASSIGNEE_ID));
33        duplicate.setCloser(new Closer(CLOSER_ID));
34        duplicate.setCreator(new Creator(CREATOR_ID));
35        duplicate.setModifier(new Modifier(MODIFIER_ID));
36        duplicate.setTitle(TITLE);
37        duplicate.setDescription(DESCRIPTION);
38        duplicate.setStatus(TaskStatus.CLOSED);
39        duplicate.setResolution(TaskResolution.DUPLICATE);
40    }
41}

When we take a look at these test methods, we notice that the only difference is that the buildTaskThatWasClosedAsDuplicate() method sets the property values of the created Task object by using a fluent API instead of setter methods.

That’s why our test data builder doesn’t make our life any easier. Before we can create a task that was closed as a duplicate, we have to know that:

  • The assignee of the task must be set.
  • The closer of the task must be set.
  • The creator of the task must be set.
  • The modifier of the task must be set.
  • The status of a closed task must be TaskStatus.CLOSED.
  • Because the task was closed as a duplicate, its resolution must be TaskResolution.DUPLICATE.

In other words, if we create stateful objects with a data-centric test data builder, we only add complexity to our test suite without getting anything in return. This is an awful deal.

Let’s find out how we can make a much better deal.

Specifying the Requirements of Our Test Data Builder

When we create a new test data builder, we want to move the object creation logic (aka business logic) from our test classes to our test data builder and provide a “foolproof” API that makes our tests easy to read, write, and maintain.

However, before we can do this, we have to identify the concrete requirements of our test data builder. This is crucial because concrete requirements give us the possibility to evaluate our test data builder and verify that it fulfills all our requirements.

We can identify the requirements of our test data builder by following these steps:

First, we have to specify all states of the created object by following these steps:

  1. Identify all states of the created object that must be supported by our test data builder.
  2. Go through these states one by one and write down the field values of the object that must be set. If a field value is irrelevant, we should ignore that field. For example, because the values of the title and description fields of a Task object are irrelevant, we will ignore them.

After we have finished this step, we should have a list of states and the field values which must be set. For example, a Task object can have the following states:

An open task:

  • The value of the assignee property must be either null or set.
  • The value of the closer property must be null.
  • The value of the creator property must be set.
  • The value of the modifier property must be set.
  • The value of the resolution property must be null.
  • The value of the status property must be TaskStatus.OPEN.

A task that is in progress:

  • The value of the assignee property must be set.
  • The value of the closer property must be null.
  • The value of the creator property must be set.
  • The value of the modifier property must be set.
  • The value of the resolution property must be null.
  • The value of the status property must be TaskStatus.IN_PROGRESS.

A closed task:

  • The value of the assignee property must be set.
  • The value of the closer property must be set.
  • The value of the creator property must be set.
  • The value of the modifier property must be set.
  • The value of the resolution property must be either TaskResolution.DONE, TaskResolution.DUPLICATE, or TaskResolution.WONT_DO.
  • The value of the status property must be TaskStatus.CLOSED.

Second, we have to specify the requirements of our test data builder by following these steps:

  1. Identify the field (or fields) that specifies the state of our object. For example, the status property specifies the state of a Task object. Because configuring the state of our object is the most complex part of the object creation logic, we must put this logic to methods which are added to our test data builder class.
  2. Identify the fields which are required by multiple states and don’t “specify the state”. For example, the assignee, creator, and modifier properties of a Task object are required by multiple states, but they don’t specify the state of an object (the status property does it). Because these properties are required by more than one state, we have to set their values by using data-centric methods.
  3. Identify the fields which are required by only one state and don’t “specify the state”. For example, the closer and resolution properties of a Task object must be set only when the task is closed. Because these properties are required by only one state, we have to set their values in the method that sets the state of the created object.
  4. Identify the remaining fields. We will set the values of these fields by using data-centric methods.

After we have finished these steps, we should have the concrete requirements of our test data builder. For example, the requirements of a test data builder that creates new Task objects are:

  • Our test data builder must have data-centric methods which set the values of the assignee, creator, description, modifier, id, and title properties.
  • Our test data builder must have methods which set the status of the created Task object. These methods must hide the complex business logic from the users of our test data builder.
  • The user of our test data builder can set the values of the closer and resolution properties only if the user is creating a closed task.

Next, we will create a test data builder that fulfills these requirements.

Creating Our New Test Data Builder

We can create our new test data builder by making the following changes to our data-centric test data builder:

I like to create a data-centric test data builder and modify it to fulfill my requirements. If I create a few objects with this test data builder before I specify the requirements of my test data builder, I have a lot better understanding of the problem I must solve.

First, we have to remove the methods which set the values of the closer, status, and resolution properties. After we have done this, our test data builder class should have only the data-centric methods which set the values of the assignee, creator, description, modifier, id, and title properties.

After we have removed these methods from our test data builder, the source code of our test data builder class looks as follows:

 1public final class TaskBuilder {
 2
 3    private Assignee assignee;
 4    private Closer closer;
 5    private Creator creator;
 6    private String description;
 7    private Modifier modifier;
 8    private Long id;
 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.assignee = new Assignee(assigneeId);
19        return this;
20    }
21
22    public TaskBuilder withCreator(Long creatorId) {
23        this.creator = new Creator(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.modifier = modifier;
39        return this;
40    }
41
42    public TaskBuilder withTitle(String title) {
43        this.title = title;
44        return this;
45    }
46
47    public Task build() {
48        Task task = new Task();
49
50        task.setAssignee(assignee);
51        task.setCloser(closer);
52        task.setCreator(creator);
53        task.setDescription(description);
54        task.setModifier(modifier);
55        task.setId(id);
56        task.setResolution(resolution);
57        task.setStatus(status);
58        task.setTitle(title);
59
60        return task;
61    }
62}

Second, we have to write a method that must be invoked when we want to create an open task. This method takes no method parameters, and it sets the value of the status property to TaskStatus.OPEN. Also, because an open task doesn’t have a closer or a resolution, our method must set the values of these properties to null.

After we have added this method to our test data builder class, our test data builder class looks as follows:

 1public final class TaskBuilder {
 2
 3    private Assignee assignee;
 4    private Closer closer;
 5    private Creator creator;
 6    private String description;
 7    private Modifier modifier;
 8    private Long id;
 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.assignee = new Assignee(assigneeId);
19        return this;
20    }
21
22    public TaskBuilder withCreator(Long creatorId) {
23        this.creator = new Creator(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.modifier = modifier;
39        return this;
40    }
41
42    public TaskBuilder withStatusOpen() {
43        this.closer = null;
44        this.resolution = null;
45        this.status = TaskStatus.OPEN;
46        return this;
47    }
48
49    public TaskBuilder withTitle(String title) {
50        this.title = title;
51        return this;
52    }
53
54    public Task build() {
55        Task task = new Task();
56
57        task.setAssignee(assignee);
58        task.setCloser(closer);
59        task.setCreator(creator);
60        task.setDescription(description);
61        task.setModifier(modifier);
62        task.setId(id);
63        task.setResolution(resolution);
64        task.setStatus(status);
65        task.setTitle(title);
66
67        return task;
68    }
69}

Third, we have to write a method that must be invoked when we want to create a task that’s in progress. This method takes no method parameters, and it sets the value of the status property to TaskStatus.IN_PROGRESS. Also, because the created task cannot have a closer or a resolution, our method must set the values of these properties to null.

After we have added this method to our test data builder class, our test data builder class looks as follows:

 1public final class TaskBuilder {
 2
 3    private Assignee assignee;
 4    private Closer closer;
 5    private Creator creator;
 6    private String description;
 7    private Modifier modifier;
 8    private Long id;
 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.assignee = new Assignee(assigneeId);
19        return this;
20    }
21
22    public TaskBuilder withCreator(Long creatorId) {
23        this.creator = new Creator(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.modifier = modifier;
39        return this;
40    }
41
42    public TaskBuilder withStatusInProgress() {
43        this.closer = null;
44        this.resolution = null;
45        this.status = TaskStatus.IN_PROGRESS;
46        return this;
47    }
48
49    public TaskBuilder withStatusOpen() {
50        this.closer = null;
51        this.resolution = null;
52        this.status = TaskStatus.OPEN;
53        return this;
54    }
55
56    public TaskBuilder withTitle(String title) {
57        this.title = title;
58        return this;
59    }
60
61    public Task build() {
62        Task task = new Task();
63
64        task.setAssignee(assignee);
65        task.setCloser(closer);
66        task.setCreator(creator);
67        task.setDescription(description);
68        task.setModifier(modifier);
69        task.setId(id);
70        task.setResolution(resolution);
71        task.setStatus(status);
72        task.setTitle(title);
73
74        return task;
75    }
76}

Fourth, we have to write the methods which are used to create closed tasks. We have to write three methods because a closed task can have three different resolutions. Of course, we could use just one method and pass the closerId and resolution as method parameters. If we would do this, our method would look as follows:

1public TaskBuilder withStatusClosed(Long closerId, TaskResolution resolution) {
2    this.closer = new Closer(closerId);
3    this.resolution = resolution;
4    this.status = TaskStatus.CLOSED;
5}

Although using this method is a better choice than using the three methods found from our data-centric test data builder, I think that we shouldn’t use this method because of one simple reason: we have a better choice.

Because we know that only a closed task can have a resolution, we can identify the target resolution in the name of our method. This means that we don’t have to pass the resolution as a method parameter.

Next, we will write three different methods (one per resolution) which take the id of the closer as a method parameter. These methods are:

  • The withResolutionDone() method creates a task that was closed because it was finished.
  • The withResolutionDuplicate() method creates a task that was closed as a duplicate.
  • The withResolutionWontDo() method creates a task what was closed because it won’t be done.

After we have added these methods to our test data builder class, the source code of our test data builder looks as follows:

 1public final class TaskBuilder {
 2
 3    private Assignee assignee;
 4    private Closer closer;
 5    private Creator creator;
 6    private String description;
 7    private Modifier modifier;
 8    private Long id;
 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.assignee = new Assignee(assigneeId);
19        return this;
20    }
21
22    public TaskBuilder withCreator(Long creatorId) {
23        this.creator = new Creator(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.modifier = modifier;
39        return this;
40    }
41
42    public TaskBuilder withResolutionDone(Long closerId) {
43        this.closer = new Closer(closerId);
44        this.resolution = TaskResolution.DONE;
45        this.status = TaskStatus.CLOSED;
46        return this;
47    }
48
49    public TaskBuilder withResolutionDuplicate(Long closerId) {
50        this.closer = new Closer(closerId);
51        this.resolution = TaskResolution.DUPLICATE;
52        this.status = TaskStatus.CLOSED;
53        return this;
54    }
55
56    public TaskBuilder withResolutionWontDo(Long closerId) {
57        this.closer = new Closer(closerId);
58        this.resolution = TaskResolution.WONT_DO;
59        this.status = TaskStatus.CLOSED;
60        return this;
61    }
62
63    public TaskBuilder withStatusInProgress() {
64        this.closer = null;
65        this.resolution = null;
66        this.status = TaskStatus.IN_PROGRESS;
67        return this;
68    }
69
70    public TaskBuilder withStatusOpen() {
71        this.closer = 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        Task task = new Task();
84
85        task.setAssignee(assignee);
86        task.setCloser(closer);
87        task.setCreator(creator);
88        task.setDescription(description);
89        task.setModifier(modifier);
90        task.setId(id);
91        task.setResolution(resolution);
92        task.setStatus(status);
93        task.setTitle(title);
94
95        return task;
96    }
97}

We have now created our new test data builder. Let’s move on and create new Task objects with it.

Creating Test Data With Our New Test Data Builder

It is time to test our new test data builder and evaluate if it helps us to write tests which are easy to read, write, and maintain. We will do this by creating three different Task objects.

These Task objects have the same states as the Task objects which we created when we evaluated the new keyword and factory methods. The following code samples contain the old test methods as well because I want that you can compare them.

First, we have to create an open task that’s not assigned to anyone. After we have created this Task object, the source code of our test class looks as follows:

 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 createOpenTaskWithoutAssigneeBuilder() {
15        Task openTask = new TaskBuilder()
16                .withId(ID)
17                .withCreator(CREATOR_ID)
18                .withModifier(MODIFIER_ID)
19                .withTitle(TITLE)
20                .withDescription(DESCRIPTION)
21                .withStatusOpen()
22                .build();
23    }
24
25    @Test
26    public void createOpenTaskWithoutAssigneeNew() {
27        Task openTask = new Task();
28        openTask.setId(ID);
29        openTask.setAssignee(null);
30        openTask.setCloser(null);
31        openTask.setCreator(new Creator(CREATOR_ID));
32        openTask.setModifier(new Modifier(MODIFIER_ID));
33        openTask.setTitle(TITLE);
34        openTask.setDescription(DESCRIPTION);
35        openTask.setStatus(TaskStatus.OPEN);
36        openTask.setResolution(null);
37    }
38
39    @Test
40    public void createOpenTaskWithoutAssigneeFactory() {
41        Task openTask = TaskFactory.createOpenTaskWithoutAssignee(ID,
42                CREATOR_ID,
43                MODIFIER_ID,
44                TITLE,
45                DESCRIPTION
46        );
47    }
48}

Second, we have to create an open task that has an assignee. After we have created this Task object, the source code of our test class looks as follows:

 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 createOpenTaskThatIsAssignedToAssigneeBuilder() {
15        Task assignedTask = new TaskBuilder()
16                .withId(ID)
17                .withAssignee(ASSIGNEE_ID)
18                .withCreator(CREATOR_ID)
19                .withModifier(MODIFIER_ID)
20                .withTitle(TITLE)
21                .withDescription(DESCRIPTION)
22                .withStatusOpen()
23                .build();
24    }
25
26    @Test
27    public void createOpenTaskThatIsAssignedToAssigneeNew() {
28        Task assignedTask = new Task();
29        assignedTask.setId(ID);
30        assignedTask.setAssignee(new Assignee(ASSIGNEE_ID));
31        assignedTask.setCloser(null);
32        assignedTask.setCreator(new Creator(CREATOR_ID));
33        assignedTask.setModifier(new Modifier(MODIFIER_ID));
34        assignedTask.setTitle(TITLE);
35        assignedTask.setDescription(DESCRIPTION);
36        assignedTask.setStatus(TaskStatus.OPEN);
37        assignedTask.setResolution(null);
38    }
39
40    @Test
41    public void createOpenTaskThatIsAssignedToAssigneeFactory() {
42        Task assignedTask = TaskFactory
43                .createOpenTaskThatIsAssignedToAssignee(ID,
44                        ASSIGNEE_ID,
45                        CREATOR_ID,
46                        MODIFIER_ID,
47                        TITLE,
48                        DESCRIPTION
49                );
50    }
51}

Third, we have to create a task that was closed as a duplicate. After we have created this Task object, the source code of our test class looks as follows:

 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 createTaskThatWasClosedAsDuplicateBuilder() {
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
26    @Test
27    public void createTaskThatClosedAsDuplicateNew() {
28        Task duplicate = new Task();
29        duplicate.setId(ID);
30        duplicate.setAssignee(new Assignee(ASSIGNEE_ID));
31        duplicate.setCloser(new Closer(CLOSER_ID));
32        duplicate.setCreator(new Creator(CREATOR_ID));
33        duplicate.setModifier(new Modifier(MODIFIER_ID));
34        duplicate.setTitle(TITLE);
35        duplicate.setDescription(DESCRIPTION);
36        duplicate.setStatus(TaskStatus.CLOSED);
37        duplicate.setResolution(TaskResolution.DUPLICATE);
38    }
39
40    @Test
41    public void createTaskThatWasClosedAsDuplicateFactory() {
42        Task duplicate = TaskFactory.createTaskThatWasClosedAsDuplicate(ID,
43                ASSIGNEE_ID,
44                CREATOR_ID,
45                CLOSER_ID,
46                MODIFIER_ID,
47                TITLE,
48                DESCRIPTION
49        );
50    }
51}

When we compare our test methods with the methods that use either the new keyword or an object mother class, we notice that:

  • The test methods which use our new test data builder are easier to read, write and maintain than the test methods which use the new keyword. The test methods which use the new keyword are a somewhat easy to read, but the problem is that they are quite painful to write and maintain because our business logic is scattered accross our test methods.
  • The test methods which use our new test data builder are easier to read and write than the methods which use an object mother class. Even though the object mother class hides our business logic from the person who uses it, its problem is that that the factory methods have “too many” methods parameters.

If we want to ensure that we cannot create objects which don’t have specific field values, we can modify the build() method to throw an exception if the field values aren’t set. For example, if we want to ensure that we cannot create a task which doesn’t have a creator, we have to write a build() method that looks as follows:

 1public Task build() {
 2    Task task = new Task();
 3
 4    task.setAssignee(assignee);
 5    task.setCloser(closer);
 6    task.setCreator(creator);
 7    task.setDescription(description);
 8    task.setModifier(modifier);
 9    task.setId(id);
10    task.setResolution(resolution);
11    task.setStatus(status);
12    task.setTitle(title);
13
14	if (task.getCreator() == null) {
15        throw new NullPointerException(
16                "The creator of a task must be set"
17        );
18    }
19
20    return task;
21}

Next, we will find out when we should use test data builders for creating stateful objects.

Should We Use a Test Data Builder?

When we decide if we should create stateful 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 we cannot create our test data by using an object mother class because our factory methods would have “too many” method parameters, we should use a test data builder.
  • If the stateful object has only one field whose value must be changed when we change the state of the object, we shouldn’t use a test data builder. If we use a test data builder in this situation, we only add complexity to our test suite without getting anything in return. That’s why we should create our test data by using the new keyword or a factory method.
  • If the stateful objects are used by only one test class or a few test classes, we shouldn’t use a test data builder. If we are in this situation, the cost of creating a test data builder is most likely higher than its benefits. That’s why we should create our test data by using the new keyword or a factory method.

We can now create stateful 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 five things:

  • We shouldn’t create stateful objects with a data-centric test data builder.
  • When we create a new test data builder, we must move our business logic from our test classes to our test data builder.
  • If we cannot create our test data by using an object mother class because our factory methods would have “too many” method parameters, we should use a test data builder.
  • If the stateful object has only one field whose value must be changed when we change the state of the object, we shouldn’t use a test data builder.
  • If the stateful objects are used by only one test class or a few test classes, we shouldn’t use a test data builder.

Get the source code from Github