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.
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 )
- You can create data container objects with test data builders
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 thenewkeyword
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:
- Identify all states of the created object that must be supported by our test data builder.
- 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
titleanddescriptionfields of aTaskobject 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
assigneeproperty must be eithernullor set. - The value of the
closerproperty must benull. - The value of the
creatorproperty must be set. - The value of the
modifierproperty must be set. - The value of the
resolutionproperty must be null. - The value of the
statusproperty must beTaskStatus.OPEN.
A task that is in progress:
- The value of the
assigneeproperty must be set. - The value of the
closerproperty must benull. - The value of the
creatorproperty must be set. - The value of the
modifierproperty must be set. - The value of the
resolutionproperty must be null. - The value of the
statusproperty must beTaskStatus.IN_PROGRESS.
A closed task:
- The value of the
assigneeproperty must be set. - The value of the
closerproperty must be set. - The value of the
creatorproperty must be set. - The value of the
modifierproperty must be set. - The value of the
resolutionproperty must be eitherTaskResolution.DONE,TaskResolution.DUPLICATE, orTaskResolution.WONT_DO. - The value of the
statusproperty must beTaskStatus.CLOSED.
Second, we have to specify the requirements of our test data builder by following these steps:
- Identify the field (or fields) that specifies the state of our object. For example, the
statusproperty specifies the state of aTaskobject. 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. - Identify the fields which are required by multiple states and don’t “specify the state”. For example, the
assignee,creator, andmodifierproperties of aTaskobject are required by multiple states, but they don’t specify the state of an object (thestatusproperty does it). Because these properties are required by more than one state, we have to set their values by using data-centric methods. - Identify the fields which are required by only one state and don’t “specify the state”. For example, the
closerandresolutionproperties of aTaskobject 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. - 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, andtitleproperties. - Our test data builder must have methods which set the
statusof the createdTaskobject. 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
closerandresolutionproperties 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:
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}
When we name these methods, we must use names which identify the state of the created object.
Additional Reading:
- Building a fluent API (internal DSL)
- Creating DSLs in Java, Part 1: What is a domain-specific language?
- Creating DSLs in Java, Part 2: Fluency and context
- Creating DSLs in Java, Part 3: Internal and external DSLs
- Creating DSLs in Java, Part 4: Where metaprogramming matters
- The Java Fluent API Designer Crash Course
- Tricks with Test Data Builders: Combining Builders
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.
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
newkeyword. The test methods which use thenewkeyword 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
newkeyword 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
newkeyword 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.