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:

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 closer field must be null.
  • The value of the status field must be TaskStatus.OPEN.
  • The resolution field must be null.

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 must always use “insane” default values because this makes it clear that our test case doesn’t care about the values of these fields.

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 :

  1. Set the default value of the creatorId field to -1L. Because the id of the creator cannot be negative, it’s clear that our test case doesn’t care about the creator of the task.
  2. Set the default value of the title field to "NOT IMPORTANT". I think that this value makes it clear that our test case doesn’t care about the title of 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 modifierId field because our builder uses the creatorId when it sets the modifier of the created Task object.
  • 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}
If you want to add these methods to an object mother class, feel free to do so. I added these methods to our test data builder class because I wrote only three factory methods. If the number of factory methods grow, it’s a good idea to move these factory methods to an object mother class. This ensures that the factory methods won’t clutter the source code of the test data builder class.

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.

Get the source code from Github