Emphasizing the State of the Created Object
After we have finished this lesson, we:
- Understand why we must emphasize the state of the created object.
- Can create a test data builder that emphasizes the state of the created object.
- Can write factory methods which emphasize the state of created object and help us to deal with the variation found from our test data.
This lesson assumes that:
- You are familiar with the system under test
- 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
- You can create stateful objects with test data builders
- You can override a builder with a test data builder
In the context of this lesson, a state of an object is specified by the value of one field or the values of multiple fields. For example, if a Task object is open:
- The
closerfield must benull. - The value of the
statusfield must beTaskStatus.OPEN. - The
resolutionfield must benull.
Get the source code from Github
Too Many Details Make Us Blind
Let’s refresh our memory and take a look at a test class that has three test methods:
- The first test method creates an open task that isn’t assigned to anyone.
- The second test method creates an open task that’s assigned to a person.
- The third test method creates a task that was closed as a duplicate.
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 = "A test data builder example";
11 private static final String DESCRIPTION = "Write an example project";
12
13 @Test
14 public void createOpenTaskWithoutAssignee() {
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 createOpenTaskThatIsAssignedToAssignee() {
27 Task assignedTask = new TaskBuilder()
28 .withId(ID)
29 .withAssignee(ASSIGNEE_ID)
30 .withCreator(CREATOR_ID)
31 .withModifier(MODIFIER_ID)
32 .withTitle(TITLE)
33 .withDescription(DESCRIPTION)
34 .withStatusOpen()
35 .build();
36 }
37
38 @Test
39 public void createTaskThatWasClosedAsDuplicate() {
40 Task duplicate = new TaskBuilder()
41 .withId(ID)
42 .withAssignee(ASSIGNEE_ID)
43 .withCreator(CREATOR_ID)
44 .withModifier(MODIFIER_ID)
45 .withTitle(TITLE)
46 .withDescription(DESCRIPTION)
47 .withResolutionDuplicate(CLOSER_ID)
48 .build();
49 }
50}
These test methods make sense if we are writing tests which ensure that the update feature is working as expected. However, there are situations when we care only about the state of the created object.
If we are writing tests for code which uses the state of the created Task object when it decides the performed action, we shouldn’t create our test data by using this technique because the code that creates new Task objects has a lot of noise that hides the essence of our test case.
For example, we might not care about the assignee, closer, creator, description, id, modifier or title of the created Task object, but our test methods must still set the values of these fields when they create new Task objects.
Next, we will get rid of this noise.
Using Default Values
We can get rid of unnecessary noise by using one of these two options:
First, if the created object uses the JavaBeans style construction , we can simply choose to not invoke the unnecessary methods of our test data builder. If we use this approach, the source code of our test class looks as follows:
1import org.junit.Test;
2
3public class TaskTest {
4
5 private static final Long ASSIGNEE_ID = 99L;
6 private static final Long CLOSER_ID = 55L;
7
8 @Test
9 public void createOpenTaskWithoutAssignee() {
10 Task openTask = new TaskBuilder()
11 .withStatusOpen()
12 .build();
13 }
14
15 @Test
16 public void createOpenTaskThatIsAssignedToAssignee() {
17 Task assignedTask = new TaskBuilder()
18 .withAssignee(ASSIGNEE_ID)
19 .withStatusOpen()
20 .build();
21 }
22
23 @Test
24 public void createTaskThatWasClosedAsDuplicate() {
25 Task duplicate = new TaskBuilder()
26 .withAssignee(ASSIGNEE_ID)
27 .withResolutionDuplicate(CLOSER_ID)
28 .build();
29 }
30}
Our test class looks a lot cleaner, but the problem is that the created Task objects are in a weird state because some of the required fields are null (creator, modifier, and title). That’s why I think that we shouldn’t use this technique.
Second, if the created object uses the builder pattern , the odds are that the builder ensures that we cannot create an object that is in an invalid state. For example, our builder might ensure that:
- We cannot create a task that has no
creator. - We cannot create a task that has no
modifier. - We cannot create a task that has no
title. - We cannot create a task that has an empty
title.
If our builder does these sanity checks, 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 Task task = new Task(this);
74
75 checkCreator(task);
76 checkModifier(task);
77 checkTitle(task);
78
79 return task;
80 }
81
82 private void checkCreator(Task task) {
83 Creator creator = task.getCreator();
84 if (creator == null) {
85 throw new NullPointerException("Creator cannot be null");
86 }
87 }
88
89 private void checkModifier(Task task) {
90 Modifier modifier = task.getModifier();
91 if (modifier == null) {
92 throw new NullPointerException("Modifier cannot be null");
93 }
94 }
95
96 private void checkTitle(Task task) {
97 String title = task.getTitle();
98 if (title == null) {
99 throw new NullPointerException("Title cannot be null");
100 }
101
102 if (title.isEmpty()) {
103 throw new IllegalStateException("Title cannot be empty");
104 }
105 }
106 }
107}
When we take a look at the Task class, we notice that we must invoke the methods which set the values of the required fields when we create a new Task object. If we don’t set the values of these fields, our builder throws an exception and our test fails.
Luckily, there is an easy fix to this problem.
Because we don’t want to invoke unnecessary methods in our test methods, and we must set the values of the required fields or the builder provided by the Task class throws an exception, we have to specify the default field values which are used if we don’t set the values of the required fields in our test method.
We can configure the default values of the required fields by making the following changes to the test data builder which we created in the previous lesson :
- Set the default value of the
creatorIdfield to-1L. Because the id of the creator cannot be negative, it’s clear that our test case doesn’t care about thecreatorof the task. - Set the default value of the
titlefield to"NOT IMPORTANT". I think that this value makes it clear that our test case doesn’t care about thetitleof the task.
After we have made these changes to our test data builder, its source code looks as follows:
1import java.lang.reflect.Field;
2
3public final class TaskBuilder {
4
5 private static final Long NOT_IN_USE = -1L;
6 private static final String NOT_IMPORTANT = "NOT IMPORTANT";
7
8 private Long assigneeId;
9 private Long closerId;
10 private Long creatorId = NOT_IN_USE;
11 private String description;
12 private Long id;
13 private Long modifierId;
14 private TaskResolution resolution;
15 private TaskStatus status;
16 private String title = NOT_IMPORTANT;
17
18 public TaskBuilder() {
19 }
20
21 public TaskBuilder withAssignee(Long assigneeId) {
22 this.assigneeId = assigneeId;
23 return this;
24 }
25
26 public TaskBuilder withCreator(Long creatorId) {
27 this.creatorId = creatorId;
28 return this;
29 }
30
31 public TaskBuilder withDescription(String description) {
32 this.description = description;
33 return this;
34 }
35
36 public TaskBuilder withId(Long id) {
37 this.id = id;
38 return this;
39 }
40
41 public TaskBuilder withModifier(Long modifierId) {
42 this.modifierId = modifierId;
43 return this;
44 }
45
46 public TaskBuilder withResolutionDone(Long closerId) {
47 this.closerId = closerId;
48 this.resolution = TaskResolution.DONE;
49 this.status = TaskStatus.CLOSED;
50 return this;
51 }
52
53 public TaskBuilder withResolutionDuplicate(Long closerId) {
54 this.closerId = closerId;
55 this.resolution = TaskResolution.DUPLICATE;
56 this.status = TaskStatus.CLOSED;
57 return this;
58 }
59
60 public TaskBuilder withResolutionWontDo(Long closerId) {
61 this.closerId = closerId;
62 this.resolution = TaskResolution.WONT_DO;
63 this.status = TaskStatus.CLOSED;
64 return this;
65 }
66
67 public TaskBuilder withStatusInProgress() {
68 this.closerId = null;
69 this.resolution = null;
70 this.status = TaskStatus.IN_PROGRESS;
71 return this;
72 }
73
74 public TaskBuilder withStatusOpen() {
75 this.closerId = null;
76 this.resolution = null;
77 this.status = TaskStatus.OPEN;
78 return this;
79 }
80
81 public TaskBuilder withTitle(String title) {
82 this.title = title;
83 return this;
84 }
85
86 public Task build() {
87 Task task = Task.getBuilder()
88 .withAssignee(assigneeId)
89 .withCreator(creatorId)
90 .withDescription(description)
91 .withId(id)
92 .withTitle(title)
93 .build();
94
95 if (closerId != null) {
96 setFieldValue(task, "closer", new Closer(closerId));
97 }
98
99 if (modifierId != null) {
100 setFieldValue(task, "modifier", new Modifier(modifierId));
101 }
102
103 setFieldValue(task, "status", this.status);
104 setFieldValue(task, "resolution", this.resolution);
105
106 return task;
107 }
108
109 private void setFieldValue(Task target,
110 String fieldName,
111 Object fieldValue) {
112 try {
113 Field targetField = target.getClass()
114 .getDeclaredField(fieldName);
115
116 targetField.setAccessible(true);
117 targetField.set(target, fieldValue);
118 }
119 catch (Exception ex) {
120 throw new RuntimeException(
121 String.format(
122 "Cannot set the value of the field: %s",
123 fieldName
124 ),
125 ex
126 );
127 }
128 }
129}
There are two things I want to point out:
- We don’t have to set the default value of the
modifierIdfield because our builder uses thecreatorIdwhen it sets themodifierof the createdTaskobject. - We should use “insane” default values even if the created object doesn’t “force” us to set the values of the required fields. In other words, I think that we should use “insane” default values even if the created object uses the JavaBeans style construction.
We can now remove unnecessary noise from our test class. After we have done this, the source code of our test class looks as follows:
1import org.junit.Test;
2
3public class TaskTest {
4
5 private static final Long ASSIGNEE_ID = 99L;
6 private static final Long CLOSER_ID = 55L;
7
8 @Test
9 public void createOpenTaskWithoutAssignee() {
10 Task openTask = new TaskBuilder()
11 .withStatusOpen()
12 .build();
13 }
14
15 @Test
16 public void createOpenTaskThatIsAssignedToAssignee() {
17 Task assignedTask = new TaskBuilder()
18 .withAssignee(ASSIGNEE_ID)
19 .withStatusOpen()
20 .build();
21 }
22
23 @Test
24 public void createTaskThatWasClosedAsDuplicate() {
25 Task duplicate = new TaskBuilder()
26 .withAssignee(ASSIGNEE_ID)
27 .withResolutionDuplicate(CLOSER_ID)
28 .build();
29 }
30}
As we can see, our test class looks clean. However, we can make it even better. Next, we will find out how we can emphasize the state of the created object by using factory methods.
Factory Methods to the Rescue
As we remember, factory methods are good at emphasizing the state of the created object, but they aren’t very good at dealing with variation found from our test data. We can solve this problem by writing factory methods which use a test data builder.
Next, we will add three public and static methods to our test data builder class:
- The
openTaskWithoutAssignee()method returns an open task that’s not assigned to anyone. - The
openTaskWithAssignee()method returns an open task that’s assigned to the given person. - The
closedAsDuplicate()method returns a closed task that was closed by the specified person.
After we have added these factory methods to our test data builder class, its source code looks as follows:
1import java.lang.reflect.Field;
2
3public final class TaskBuilder {
4
5 private static final Long NOT_IN_USE = -1L;
6 private static final String NOT_IMPORTANT = "NOT IMPORTANT";
7
8 private Long assigneeId;
9 private Long closerId;
10 private Long creatorId = NOT_IN_USE;
11 private String description;
12 private Long id;
13 private Long modifierId;
14 private TaskResolution resolution;
15 private TaskStatus status;
16 private String title = NOT_IMPORTANT;
17
18 public static Task openTaskWithoutAssignee() {
19 return new TaskBuilder()
20 .withStatusOpen()
21 .build();
22 }
23
24 public static Task openTaskWithAssignee(Long assigneeId) {
25 return new TaskBuilder()
26 .withAssignee(assigneeId)
27 .withStatusOpen()
28 .build();
29 }
30
31 public static Task closedAsDuplicate(Long closerId) {
32 return new TaskBuilder()
33 .withAssignee(NOT_IN_USE)
34 .withResolutionDuplicate(closerId)
35 .build();
36 }
37
38 public TaskBuilder() {
39 }
40
41 public TaskBuilder withAssignee(Long assigneeId) {
42 this.assigneeId = assigneeId;
43 return this;
44 }
45
46 public TaskBuilder withCreator(Long creatorId) {
47 this.creatorId = creatorId;
48 return this;
49 }
50
51 public TaskBuilder withDescription(String description) {
52 this.description = description;
53 return this;
54 }
55
56 public TaskBuilder withId(Long id) {
57 this.id = id;
58 return this;
59 }
60
61 public TaskBuilder withModifier(Long modifierId) {
62 this.modifierId = modifierId;
63 return this;
64 }
65
66 public TaskBuilder withResolutionDone(Long closerId) {
67 this.closerId = closerId;
68 this.resolution = TaskResolution.DONE;
69 this.status = TaskStatus.CLOSED;
70 return this;
71 }
72
73 public TaskBuilder withResolutionDuplicate(Long closerId) {
74 this.closerId = closerId;
75 this.resolution = TaskResolution.DUPLICATE;
76 this.status = TaskStatus.CLOSED;
77 return this;
78 }
79
80 public TaskBuilder withResolutionWontDo(Long closerId) {
81 this.closerId = closerId;
82 this.resolution = TaskResolution.WONT_DO;
83 this.status = TaskStatus.CLOSED;
84 return this;
85 }
86
87 public TaskBuilder withStatusInProgress() {
88 this.closerId = null;
89 this.resolution = null;
90 this.status = TaskStatus.IN_PROGRESS;
91 return this;
92 }
93
94 public TaskBuilder withStatusOpen() {
95 this.closerId = null;
96 this.resolution = null;
97 this.status = TaskStatus.OPEN;
98 return this;
99 }
100
101 public TaskBuilder withTitle(String title) {
102 this.title = title;
103 return this;
104 }
105
106 public Task build() {
107 Task task = Task.getBuilder()
108 .withAssignee(assigneeId)
109 .withCreator(creatorId)
110 .withDescription(description)
111 .withId(id)
112 .withTitle(title)
113 .build();
114
115 if (closerId != null) {
116 setFieldValue(task, "closer", new Closer(closerId));
117 }
118
119 if (modifierId != null) {
120 setFieldValue(task, "modifier", new Modifier(modifierId));
121 }
122
123 setFieldValue(task, "status", this.status);
124 setFieldValue(task, "resolution", this.resolution);
125
126 return task;
127 }
128
129 private void setFieldValue(Task target,
130 String fieldName,
131 Object fieldValue) {
132 try {
133 Field targetField = target.getClass()
134 .getDeclaredField(fieldName);
135
136 targetField.setAccessible(true);
137 targetField.set(target, fieldValue);
138 }
139 catch (Exception ex) {
140 throw new RuntimeException(
141 String.format(
142 "Cannot set the value of the field: %s",
143 fieldName
144 ),
145 ex
146 );
147 }
148 }
149}
We can now modify our test methods to use our new factory methods. After we have done the required changes, the source code of our test class looks as follows:
1import org.junit.Test;
2
3public class TaskTest {
4
5 private static final Long ASSIGNEE_ID = 99L;
6 private static final Long CLOSER_ID = 55L;
7
8 @Test
9 public void createOpenTaskWithoutAssignee() {
10 Task openTask = TaskBuilder.openTaskWithoutAssignee();
11 }
12
13 @Test
14 public void createOpenTaskThatIsAssignedToAssignee() {
15 Task assignedTask = TaskBuilder.openTaskWithAssignee(ASSIGNEE_ID);
16 }
17
18 @Test
19 public void createTaskThatWasClosedAsDuplicate() {
20 Task duplicate = TaskBuilder.closedAsDuplicate(CLOSER_ID);
21 }
22}
As we can see, our new test methods look really clean and the factory methods help us to emphasize the state of the created objects.
Let’s move on and find out when we should emphasize the state of the created object by using factory methods.
When Should We Use Factory Methods?
I think that we should use a factory method only if we can write a factory method that has only one method parameter or no method parameters. We shouldn’t write factory methods which have more than one method parameter because of these two reasons:
- Because the purpose of the factory method isn’t crystal clear, it’s hard to figure out a good name for it. This means that the name of the factory method doesn’t most likely identify the most important property of the created object.
- As we remember , we shouldn’t create our test data with a factory method if the factory method has too many method parameters because using this factory method makes our tests hard to read and write. Also, if we use the technique described in this lesson, we must use as few method parameters as possible because the goal of this technique is to select the most important property of the created object and emphasize the selected property.
We can now emphasize the state of the created object by using a test data builder and a factory method. Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us four things:
- If we are writing tests for code which uses the state of the created object when it decides the performed action, we must emphasize the state of the created object because this helps us to highlight the essence of our test case.
- When we create our test data by using a test data builder, we can emphasize the state of the created object by configuring the default field values which are used if we don’t set the values of the required fields in our test method.
- When we specify the default field values, we must use “insane” values because this makes it clear that our test case doesn’t care about the values of these fields.
- We should emphasize the state of the created object by using a factory method only if we can write a factory method that has only one method parameter or no method parameters.