Creating Test Data With Factory Methods: Using an Object Mother Class
After we have finished this lesson, we:
- Know when we shouldn’t create our test data with an object mother class.
- Understand when we should create our test data by using an object mother class.
This lesson assumes that:
Get the source code from Github
Creating a New Object Mother Class
An object mother class is a class that contains static methods which create objects that we can use in our test classes. We can create a new object mother class by following these steps:
- Create a
publicandfinalclass. It is a good idea to mark the created class asfinalbecause this prevents developers from extending it. - Add a
privateconstructor to the created class. This prevents developers from instantiating our object mother class.
After we have created an object mother class that creates new Task objects, the source code of our object mother class looks as follows:
1public final class TaskFactory {
2
3 private TaskFactory() {}
4}
There are two things I want to point out:
- When we name our object mother classes, we should append the string: ‘Factory’ to the type of the created object. Because our object mother class creates
Taskobjects, its name should be:TaskFactory. - This lesson assumes that we want to use this object mother class from multiple classes which aren’t necessarily in the same package as the object mother class. If this is not the case, we should limit the visibility of our object mother class.
Additional Reading:
We can now write new factory methods by adding public and static methods to our object mother class. Next, we will write factory methods which create new Task objects.
Creating Task Objects With an Object Mother Class
Let’s write four different factory methods and identify the pros and cons of these factory methods.
Example 1: A Task That Was Closed as a Duplicate
First, we will write a factory method which takes the required information as method parameters and creates a task that was closed as a duplicate. After we have written this factory method, the source code of our object mother class looks as follows:
1public final class TaskFactory {
2
3 private TaskFactory() {}
4
5 public static Task createTaskThatWasClosedAsDuplicate(Long taskId,
6 Long assigneeId,
7 Long creatorId,
8 Long closerId,
9 Long modifierId,
10 String title,
11 String description) {
12 Task task = new Task();
13 task.setId(taskId);
14 task.setAssignee(new Assignee(assigneeId));
15 task.setCloser(new Closer(closerId));
16 task.setCreator(new Creator(creatorId));
17 task.setModifier(new Modifier(modifierId));
18 task.setTitle(title);
19 task.setDescription(description);
20 task.setStatus(TaskStatus.CLOSED);
21 task.setResolution(TaskResolution.DUPLICATE);
22 return task;
23 }
24}
After we have written this factory method, we will write a test method which uses our new factory method. After we have written this test method, the source code of our test class looks as follows:
1import org.junit.Test;
2
3public class TaskObjectMotherTest {
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 = 23L;
10 private static final String TITLE = "Write an example project";
11 private static final String DESCRIPTION = "Use an object mother class";
12
13 @Test
14 public void createTaskThatWasClosedAsDuplicate() {
15 Task closedAsDuplicate = TaskFactory
16 .createTaskThatWasClosedAsDuplicate(ID,
17 ASSIGNEE_ID,
18 CREATOR_ID,
19 CLOSER_ID,
20 MODIFIER_ID,
21 TITLE,
22 DESCRIPTION
23 );
24 }
25}
The pros of this factory method are:
- It has a good name that identifies the essence of the created object.
- It hides the logic that sets the status and resolution of the created
Taskobject. - Because our factory method takes the required information as method parameters, it preserves the connection between our test method and test data.
This factory method has one drawback:
It has seven method parameters and the subsequent method parameters have the same type. This means that it can be hard to remember the order of method parameters.
Example 2: An Open Task That Is Assigned to an Assignee
Second, we will write a factory method that creates a task which is assigned to a person. Because this factory method has no method parameters, we have to add the required constants to our object mother class before we can write our factory method.
After we have written this factory method, the source code of our object mother class looks as follows:
1public final class TaskFactory {
2
3 private static final Long ID = 1L;
4 private static final Long ASSIGNEE_ID = 99L;
5 private static final Long CREATOR_ID = 44L;
6 private static final Long MODIFIER_ID = 23L;
7 private static final String TITLE = "Write an example project";
8 private static final String DESCRIPTION = "Use an object mother class";
9
10 private TaskFactory() {}
11
12 public static Task createOpenTaskThatIsAssignedToAssignee() {
13 Task task = new Task();
14 task.setId(ID);
15 task.setAssignee(new Assignee(ASSIGNEE_ID));
16 task.setCloser(null);
17 task.setCreator(new Creator(CREATOR_ID));
18 task.setModifier(new Modifier(MODIFIER_ID));
19 task.setTitle(TITLE);
20 task.setDescription(DESCRIPTION);
21 task.setStatus(TaskStatus.OPEN);
22 task.setResolution(null);
23
24 return task;
25 }
26}
After we have written this factory method, we will write a test method which uses our new factory method. After we have written this test method, the source code of our test class looks as follows:
1import org.junit.Test;
2
3public class TaskObjectMotherTest {
4
5 @Test
6 public void createOpenTaskWithAssignee() {
7 Task openTaskWithAssignee = TaskFactory
8 .createOpenTaskThatIsAssignedToAssignee();
9 }
10}
This factory method has one benefit:
It has a good name which emphasizes the fact that the created task is an open task that is assigned to someone. This works well if we care only about the fact that the created task is an open task that is assigned to someone and we aren’t interested in the actual field values of the created object.
This factory method has one drawback:
Because it has no method parameters, it sets the field values of the created object by using the constants declared by our object mother class. This breaks the connection between our test method and test data. That’s why we shouldn’t use this factory method if we care about the actual field values of the created object.
Example 3: An Open Task That Is Assigned to the Specified Person
Third, we will write a factory method which creates an open task that is assigned to the specified person. This method takes the id of the assignee as a method parameter. Also, because this factory method has only one method parameter, we have to add the required constants to our object mother class before we can write our factory method.
After we have written our factory method, the source code of our object mother class looks as follows:
1public final class TaskFactory {
2
3 private static final Long ID = 1L;
4 private static final Long CREATOR_ID = 44L;
5 private static final Long MODIFIER_ID = 23L;
6 private static final String TITLE = "Write an example project";
7 private static final String DESCRIPTION = "Use an object mother class";
8
9 private TaskFactory() {}
10
11 public static Task createOpenTaskThatIsAssignedTo(Long assigneeId) {
12 Task task = new Task();
13 task.setId(ID);
14 task.setAssignee(new Assignee(assigneeId));
15 task.setCloser(null);
16 task.setCreator(new Creator(CREATOR_ID));
17 task.setModifier(new Modifier(MODIFIER_ID));
18 task.setTitle(TITLE);
19 task.setDescription(DESCRIPTION);
20 task.setStatus(TaskStatus.OPEN);
21 task.setResolution(null);
22 return task;
23 }
24}
After we have written this factory method, we will write a test method which uses our new factory method. After we have written this test method, the source code of our test class looks as follows:
1import org.junit.Test;
2
3public class TaskObjectMotherTest {
4
5 private static final Long ASSIGNEE_ID = 99L;
6
7 @Test
8 public void createOpenTaskWithAssignee() {
9 Task openTaskWithAssignee = TaskFactory
10 .createOpenTaskThatIsAssignedTo(ASSIGNEE_ID);
11 }
12}
The pros of this factory method are:
- It has a good name that emphasizes the fact that it creates an open task that is assigned to the specified person. This works well if we care only about the fact that the created task is an open task that is assigned to the specified person, and we aren’t interested in the other field values of the created object.
- It has only one method parameter that emphasizes the assignee of the created task.
This factory method has one drawback:
It sets the “other” field values of the created object by using the constants declared by our object mother class. This breaks the connection between our test method and test data. Again, if we care about the “other” field values of the created object, we shouldn’t use this factory method.
Example 4: A Closed Task
Fourth, we will write a factory method which creates a closed task. This method takes the id of the closer and the resolution as method parameters. Also, because this factory method has only two method parameters, we have to add the required constants to our object mother class before we can write our factory method.
After we have written our factory method, the source code of our object mother class looks as follows:
1public final class TaskFactory {
2
3 private static final Long ID = 1L;
4 private static final Long ASSIGNEE_ID = 99L;
5 private static final Long CREATOR_ID = 44L;
6 private static final Long MODIFIER_ID = 23L;
7 private static final String TITLE = "Write an example project";
8 private static final String DESCRIPTION = "Use an object mother class";
9
10 private TaskFactory() {}
11
12 public static Task createClosedTask(Long closerId,
13 TaskResolution resolution) {
14 Task task = new Task();
15 task.setId(ID);
16 task.setAssignee(new Assignee(ASSIGNEE_ID));
17 task.setCloser(new Closer(closerId));
18 task.setCreator(new Creator(CREATOR_ID));
19 task.setModifier(new Modifier(MODIFIER_ID));
20 task.setTitle(TITLE);
21 task.setDescription(DESCRIPTION);
22 task.setStatus(TaskStatus.OPEN);
23 task.setResolution(resolution);
24 return task;
25 }
26}
After we have written this factory method, we will write a test method which uses our new factory method. After we have written this test method, the source code of our test class looks as follows:
1import org.junit.Test;
2
3public class TaskObjectMotherTest {
4
5 private static final Long CLOSER_ID = 55L;
6
7 @Test
8 public void createClosedTaskWithCloserAndResolution() {
9 Task closedAsDuplicate = TaskFactory
10 .createClosedTask(CLOSER_ID, TaskResolution.DONE);
11 }
12}
The pros of this factory method are:
- It has a proper name that emphasizes the fact that it creates a closed task.
- It has only two method parameters which emphasize that the created task was closed by the specified person who used the resolution given as a method parameter. This works well if we care only about the fact that the created task was closed the specified person who used the given resolution, and we don’t care about the other field values of the created object.
This factory method has one drawback:
It sets the “other” field values of the created object by using the constants declared by our object mother class. This breaks the connection between our test method and test data. That’s why we shouldn’t use this factory method if we care about the actual field values of the created object.
We have now written four factory methods which create Task objects. Let’s summarize what we learned from these examples.
What Did These Examples Teach to Us?
These examples taught us that we must pay special attention to the API of our object mother class. To be more specific, when we add factory methods to an object mother class, we have to follow these rules:
First, we have to use a method name that identifies the essence of the created object. Even though local factory methods should also have good names, using a good name is extremely important when we are adding a factory method to an object mother class because this allows us to write test methods that can be read without reading the source code of our object mother class.
Second, we have to emphasize all important properties of the created object by using method parameters. This is necessary because this helps us to highlight the properties which are important to our test cases. Again, we must remember that our goal is to write test methods that can be read without reading the source code of our object mother class.
Third, we have to document our factory methods by using Javadoc. Typically a factory method is useful only if specific conditions are met. If we cannot document these conditions by using the name of our factory method or its method parameters, we must write a Javadoc comment that documents our factory method.
If we use Javadoc for this purpose, we can help the other developers to use the correct factory method for the right purpose. This makes our test code easier to write and maintain. Also, if we add Javadoc comments to our factory methods, it might be possible to read our test code without reading the source code of our object mother class even if our factory methods don’t have an excellent API.
Before we continue, I want to point out two things:
- If a person who reads our test code has to read the source code of our object mother class, she has to open another file and switch her attention from our test case to its implementation details. We should do our best to avoid this situation because context switching can be quite expensive .
- Common sense is your most important asset and I hope that you use it. Next, I will introduce some rules that help you to decide if you should use an object mother class. These rules aren’t best practices, and you must break them if it makes sense to you.
Next, we will find out when we shouldn’t use an object mother class.
When We Shouldn’t Use an Object Mother Class
We shouldn’t use an object mother class if any of the following conditions is true:
- The created objects are used by only one test class or a few test classes. Every decision is a trade-off and using an object mother class has a cost. If we are in this situation, the cost of using an object mother class is most likely higher than its benefits. That’s why we should create our test data by using a local factory method or the
newkeyword. - The factory method has “too many” method parameters. I won’t specify an exact number because this is a decision that you have to make yourself. However, when you start making mistakes and/or creating new objects feels a bit cumbersome, your factory method most likely has too many method parameters. That’s why a factory method isn’t a good choice if our test data has a lot of variation.
- The order of method parameters is logical only to people who belong to a specific group. This is important if the test code can be read and/or written by people who think that the order of method parameters is not logical. I think that if our tests can be confusing to some people, they are not easy to read and write.
- Our test case uses properties which aren’t emphasized by the name of our factory method and its method parameters. This is important because we want to write tests that can be read without reading the source code of our object mother class. In other words, we want to avoid context switching because it can be quite expensive.
Let’s move on and find out when we should use an object mother class.
When We Should Use an Object Mother Class
We should use an object mother class if all of the following conditions are true:
- The factory method has zero or only a few method parameters. It’s a good idea to use a factory method in this situation IF we can identify the essence of the created object by using the name of our factory method and its method parameters. Also, if our factory method doesn’t have too many method parameters, it indicates that using a factory method is a good choice because our test data doesn’t have a lot of variation.
- Our test case uses only properties which are emphasized by the name of our factory method and its method parameters. It’s a good idea to use an object mother class in this situation because we can write tests that can be read without reading the source code of our object mother class. In other words, the reader of our test code doesn’t have to pay the price of context switching.
- The created objects are used by multiple test classes. We should use an object mother class in this situation because it helps us to remove duplicate code from our test classes. After we have removed duplicate code from our test suite, we notice that our test are easier to write and maintain.
We can now create new objects by using an object mother class, and we know when we should use it in our test classes. Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us three things:
- If an object mother class doesn’t make our tests easier to read and write, we shouldn’t use it.
- We should use an object mother class if the created objects are used by multiple test classes and our test cases use only properties which are emphasized by the name of our factory method and its method parameters.
- We must always trust our common sense instead of “best practices”.