Creating Test Data With Factory Methods: Using Local Factory Methods

After we have finished this lesson, we:

  • Know when we shouldn’t create our test data with local factory methods.
  • Understand when we should create our test data by using local factory methods.

Get the source code from Github

Creating MailingAddress Objects With Local Factory Methods

Before we can create new MailingAddress objects with local factory methods, we have to create a new test class and add all required constants to our test class. After we have created our test class, its source code looks as follows:

 1public class MailingAddressLocalTest {
 2
 3    private static final String CITY = "Test City";
 4    private static final String COUNTRY_UNITED_STATES = "USA";
 5    private static final String POST_CODE = "11111";
 6    private static final String POST_OFFICE_BOX = "PO Box 1";
 7    private static final String RECEIVER = "Teddy Tester";
 8    private static final String STATE = "TX";
 9    private static final String STREET_ADDRESS = "Test Street 51";
10}

Next, we will write two different factory methods and identify the pros and cons of these factory methods. Both factory methods create a new MailingAddress object which contains a “normal” US address. In other words, we will create an address that looks as follows:

1Teddy Tester
2Test Street 51
3Test City TX 11111
4USA

First, we will write a factory method that takes the required information as method parameters. After we have written this method, we have to write a test method which uses our new factory method.

After we have written both methods, the source code of our test class looks as follows:

 1import org.junit.Test;
 2
 3public class MailingAddressLocalTest {
 4
 5    private static final String CITY = "Test City";
 6    private static final String COUNTRY_UNITED_STATES = "USA";
 7    private static final String POST_CODE = "11111";
 8    private static final String POST_OFFICE_BOX = "PO Box 1";
 9    private static final String RECEIVER = "Teddy Tester";
10    private static final String STATE = "TX";
11    private static final String STREET_ADDRESS = "Test Street 51";
12
13    @Test
14    public void createNormalUSAddress() {
15        MailingAddress usAddress = createNormalUSAddress(RECEIVER,
16                STREET_ADDRESS,
17                POST_CODE,
18                CITY,
19                STATE
20        );
21    }
22
23    private MailingAddress createNormalUSAddress(String receiver,
24                                                 String streetAddress,
25                                                 String postCode,
26                                                 String city,
27                                                 String state) {
28        return new MailingAddress(receiver,
29                streetAddress,
30                null,
31                postCode,
32                city,
33                state,
34                COUNTRY_UNITED_STATES
35        );
36    }
37}

The pros of this factory method are:

  • It has a good name that identifies the essence of the created object.
  • It takes five method parameters instead of seven constructor arguments.
  • Because our factory method takes the required information as method parameters, it preserves the connection between our test method and test data.

The cons of this factory method are:

  • It has five method parameters of same type (String).
  • The order of method parameters is logical only to people who are familiar with the way Finnish people construct addresses.

Second, we will write a factory method that has no method parameters. After we have written this method, we have to write a test method which uses our new factory method.

After we have written both methods, the source code of our test class looks as follows:

 1import org.junit.Test;
 2
 3public class MailingAddressLocalTest {
 4
 5    private static final String CITY = "Test City";
 6    private static final String COUNTRY_UNITED_STATES = "USA";
 7    private static final String POST_CODE = "11111";
 8    private static final String POST_OFFICE_BOX = "PO Box 1";
 9    private static final String RECEIVER = "Teddy Tester";
10    private static final String STATE = "TX";
11    private static final String STREET_ADDRESS = "Test Street 51";
12
13    @Test
14    public void createUSAddress() {
15        MailingAddress usAddress = createNormalUSAddress();
16    }
17
18    private MailingAddress createNormalUSAddress() {
19        return new MailingAddress(RECEIVER,
20                STREET_ADDRESS,
21                null,
22                POST_CODE,
23                CITY,
24                STATE,
25                COUNTRY_UNITED_STATES
26        );
27    }
28}

This factory method has one benefit:

It has a good name that emphasizes the fact the created address is a US address that has a street address. This works well if we care only about the fact that the created object contains a US address that has a street address and we aren’t interested in the 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 test class. This breaks the connection between our test method and test data, and can make our tests hard to read.

I chose the “worst” examples mainly because we don’t have time to cover all possible scenarios during this lesson. If you want to take a closer look at these scenarios, you should take a look at the example application of this lesson .

Let’s move on and find out how we can create new Task objects with local factory methods.

Creating Task Objects With Local Factory Methods

Before we can create new Task objects with local factory methods, we have to create a new test class and add all required constants to our test class. After we have created our test class, its source code looks as follows:

 1public class TaskLocalTest {
 2
 3    private static final Long ID = 1L;
 4    private static final Long ASSIGNEE_ID = 99L;
 5    private static final Long CLOSER_ID = 55L;
 6    private static final Long CREATOR_ID = 44L;
 7    private static final Long MODIFIER_ID = 23L;
 8    private static final String TITLE = "Write an example project";
 9    private static final String DESCRIPTION = "Use local factory methods";
10}

Next, we will write three different factory methods and identify the pros and cons of these factory methods.

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 method, we have to write a test method which uses our new factory method.

After we have written both methods, the source code of our test class looks as follows:

 1import org.junit.Test;
 2
 3public class TaskLocalTest {
 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 local factory methods";
12
13    @Test
14    public void createTaskThatWasClosedAsDuplicate() {
15        Task closedAsDuplicate = createTaskThatWasClosedAsDuplicate(ID,
16                ASSIGNEE_ID,
17                CREATOR_ID,
18                CLOSER_ID,
19                MODIFIER_ID,
20                TITLE,
21                DESCRIPTION
22        );
23    }
24
25    private Task createTaskThatWasClosedAsDuplicate(Long taskId,
26                                                    Long assigneeId,
27                                                    Long creatorId,
28                                                    Long closerId,
29                                                    Long modifierId,
30                                                    String title,
31                                                    String description) {
32        Task task = new Task();
33        task.setId(taskId);
34        task.setAssignee(new Assignee(assigneeId));
35        task.setCloser(new Closer(closerId));
36        task.setCreator(new Creator(creatorId));
37        task.setModifier(new Modifier(modifierId));
38        task.setTitle(title);
39        task.setDescription(description);
40        task.setStatus(TaskStatus.CLOSED);
41        task.setResolution(TaskResolution.DUPLICATE);
42        return task;
43    }
44}

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 Task object.
  • 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.

Second, we will write a factory method which creates an open task that is assigned to a person. This method takes the id of the assignee as a method parameter. After we have written this method, we have to write a test method which uses our new factory method.

After we have written both methods, the source code of our test class looks as follows:

 1import org.junit.Test;
 2
 3public class TaskLocalTest {
 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 local factory methods";
12
13    @Test
14    public void createOpenTaskWithAssignee() {
15        Task openWithAssignee = createOpenTaskThatIsAssignedTo(ASSIGNEE_ID);
16    }
17
18    private Task createOpenTaskThatIsAssignedTo(Long assigneeId) {
19        Task task = new Task();
20        task.setId(ID);
21        task.setAssignee(new Assignee(assigneeId));
22        task.setCloser(null);
23        task.setCreator(new Creator(CREATOR_ID));
24        task.setModifier(new Modifier(MODIFIER_ID));
25        task.setTitle(TITLE);
26        task.setDescription(DESCRIPTION);
27        task.setStatus(TaskStatus.OPEN);
28        task.setResolution(null);
29        return task;
30    }
31}

The pros of this factory method are:

  • It has a good name that emphasizes the fact this method creates an open task that is assigned to the given 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 test class. This breaks the connection between our test method and test data.

Third, 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. After we have written this method, we have to write a test method which uses our new factory method.

After we have written both methods, the source code of our test class looks as follows:

 1import org.junit.Test;
 2
 3public class TaskLocalTest {
 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 local factory methods";
12
13    @Test
14    public void createClosedTask() {
15        Task closedAsDuplicate = createClosedTask(CLOSER_ID,
16                TaskResolution.DUPLICATE
17        );
18    }
19
20    private Task createClosedTask(Long closerId, TaskResolution resolution) {
21        Task task = new Task();
22        task.setId(ID);
23        task.setAssignee(new Assignee(ASSIGNEE_ID));
24        task.setCloser(new Closer(closerId));
25        task.setCreator(new Creator(CREATOR_ID));
26        task.setModifier(new Modifier(MODIFIER_ID));
27        task.setTitle(TITLE);
28        task.setDescription(DESCRIPTION);
29        task.setStatus(TaskStatus.CLOSED);
30        task.setResolution(resolution);
31        return task;
32    }
33}

The pros of this factory method are:

  • It has a good name that emphasizes the fact that this method 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 given 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 test class. This breaks the connection between our test method and test data.

We have now written factory methods which create both MailingAddress and Task objects. Let’s summarize what we learned from these examples.

What Did These Examples Teach to Us?

These examples taught us that:

First, if a local factory method has no method parameters, it breaks the connection between our test method and test data. This means that:

  • If we need to find out the field values of the created object, we have to read the source code of our factory method. This makes our code slower to read.
  • If we want to write assertions for the created object, we have to specify the expected values by using the constants declared in our test class. This looks a bit confusing because the actual field values aren’t specified in our test method.

However, sometimes we can fix these problems by giving a good name to our factory method. Also, if we are writing nested tests, we can provide context for the created objects by using good class names.

Second, if a local factory method has only a few method parameters, these method parameters help us to emphasize specific properties of the created object. In other words, these method parameters make it crystal clear that these values are important to our test case.

Third, if a local factory method has a good name and it doesn’t have “too many” method parameters, it helps us to make our code easier to read, write, and maintain. When we combine a good name with a few important method parameters, we can identify the essence of the created object and write a factory method that helps us to eliminate duplicate code because it has a flexible API.

Fourth, if a local factory method has “too many” method parameters, it isn’t a better option than the new keyword.

If the created object uses the JavaBeans style construction, this factory method makes our code harder to read and write because it doesn’t emphasize the property values in the same way as setter methods.

On other hand, if the created object uses the telescoping constructor anti-pattern, this factory method might be a slightly better choice than using the constructor, but it still has too many method parameters.

Before we continue, I want to point out that 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 local factory methods. 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 local factory methods.

When We Shouldn’t Use Local Factory Methods

We shouldn’t use local factory methods if any of the following conditions is true:

  • 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.
  • The created objects are required by multiple test classes. Even though a factory method might be the right choice, we shouldn’t use local factory methods in this situation because they add duplicate code to our test suite.

Let’s move on and find out when we should use local factory methods.

When We Should Use Local Factory Methods

We should use local factory methods if any of the following conditions is 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.
  • We care only about a specific property or a few properties of the created object. It makes sense to use a factory method in this situation because it’s likely that we can create the required object by using a factory method that has a good name which describes the essence of the created object. Also, the odds are that we can write a factory method which doesn’t have too many method parameters. By the way, I think that this is the best use case for factory methods.
  • The created objects are used by only one test class or a few test classes. Every decision is a trade-off and the other options (a test data builder or an object mother class) have a cost. If we are in this situation, the cost of using the other options is most likely higher than the benefits of using these options. That’s why we should create our test data by using local factory methods.

We can now create new objects by using local factory methods and we know when we should use them in our test classes. Let’s summarize what we learned from this lesson.

Summary

This lesson has taught us three things:

  • If local factory methods don’t make our code easy to read and write, we shouldn’t use them in our test classes.
  • If we care only about a specific property or a few properties of the created object, we should use a local factory method.
  • We must always trust our common sense instead of “best practices”.

Get the source code from Github