Creating new objects is an essential part of automated testing, and the most obvious way to do it is to use the new keyword.
However, this is not the best way to create new objects in our test cases, and using the new keyword will make our tests harder to read and maintain.
This blog post identifies the problems caused by the new keyword, and describes how we can solve these problems by using factory methods and the builder pattern.
New Is Not the New Black
During this tutorial we have been refactoring a unit test which ensures that the registerNewUserAccount(RegistrationForm userAccountData) method of the RepositoryUserService class works as expected when a new user account is created by using a unique email address and a social sign in provider.
The RegistrationForm class is a data transfer object (DTO), and our unit tests sets its property values by using setter methods. The source code of our unit test looks as follows (the relevant code is highlighted):
1import org.junit.Before;
2import org.junit.Test;
3import org.junit.runner.RunWith;
4import org.mockito.Mock;
5import org.mockito.invocation.InvocationOnMock;
6import org.mockito.runners.MockitoJUnitRunner;
7import org.mockito.stubbing.Answer;
8import org.springframework.security.crypto.password.PasswordEncoder;
9
10import static org.junit.Assert.assertEquals;
11import static org.junit.Assert.assertNull;
12import static org.mockito.Matchers.isA;
13import static org.mockito.Mockito.times;
14import static org.mockito.Mockito.verify;
15import static org.mockito.Mockito.verifyNoMoreInteractions;
16import static org.mockito.Mockito.verifyZeroInteractions;
17import static org.mockito.Mockito.when;
18
19@RunWith(MockitoJUnitRunner.class)
20public class RepositoryUserServiceTest {
21
22 private static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";
23 private static final String REGISTRATION_FIRST_NAME = "John";
24 private static final String REGISTRATION_LAST_NAME = "Smith";
25 private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;
26 private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;
27
28 private RepositoryUserService registrationService;
29
30 @Mock
31 private PasswordEncoder passwordEncoder;
32
33 @Mock
34 private UserRepository repository;
35
36 @Before
37 public void setUp() {
38 registrationService = new RepositoryUserService(passwordEncoder, repository);
39 }
40
41 @Test
42 public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {
43 RegistrationForm registration = new RegistrationForm();
44 registration.setEmail(REGISTRATION_EMAIL_ADDRESS);
45 registration.setFirstName(REGISTRATION_FIRST_NAME);
46 registration.setLastName(REGISTRATION_LAST_NAME);
47 registration.setSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
48
49 when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);
50
51 when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {
52 @Override
53 public User answer(InvocationOnMock invocation) throws Throwable {
54 Object[] arguments = invocation.getArguments();
55 return (User) arguments[0];
56 }
57 });
58
59 User createdUserAccount = registrationService.registerNewUserAccount(registration);
60
61 assertEquals(REGISTRATION_EMAIL_ADDRESS, createdUserAccount.getEmail());
62 assertEquals(REGISTRATION_FIRST_NAME, createdUserAccount.getFirstName());
63 assertEquals(REGISTRATION_LAST_NAME, createdUserAccount.getLastName());
64 assertEquals(SOCIAL_SIGN_IN_PROVIDER, createdUserAccount.getSignInProvider());
65 assertEquals(ROLE_REGISTERED_USER, createdUserAccount.getRole());
66 assertNull(createdUserAccount.getPassword());
67
68 verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);
69 verify(repository, times(1)).save(createdUserAccount);
70 verifyNoMoreInteractions(repository);
71 verifyZeroInteractions(passwordEncoder);
72 }
73}
So, what is the problem? The highlighted part of our unit test is short and it is relatively easy to read. In my opinion, the biggest problem of this code is that it is data centric. It creates a new RegistrationForm object and sets the property values of the created object, but it doesn’t describe the meaning of these property values.
If we create new objects in the test method by using the new keyword, our tests become harder to read because:
- The reader has to know the different states of the created object. For example, if we think about our example, the reader has to know that if we create a new RegistrationForm object and set the property values of the email, firstName, lastName, and signInProvider properties, it means that the object is a registration which is made by using a social sign in provider.
- If the created object has many properties, the code which creates it, litters the source code of our tests. We should remember that even though we need these objects in our tests, we should focus on describing the behavior of the tested method / feature.
Although it isn’t realistic to assume that we can completely eliminate these drawbacks, we should do our best to minimize their effect and make our tests as easy to read as possible.
Let’s find out how we can do this by using factory methods.
Using Factory Methods
When we create new objects by using factory methods, we should name the factory methods and their method parameters in a such way that it makes our code easier to read and write. Let’s take a look at two different factory methods and see what kind of an effect they have to the readability of our unit test.
The name of the first factory method is newRegistrationViaSocialSignIn(), and it has no method parameters. After we have added this factory method to our test class, the source of our unit test looks as follows (the relevant parts are highlighted):
1import org.junit.Before;
2import org.junit.Test;
3import org.junit.runner.RunWith;
4import org.mockito.Mock;
5import org.mockito.invocation.InvocationOnMock;
6import org.mockito.runners.MockitoJUnitRunner;
7import org.mockito.stubbing.Answer;
8import org.springframework.security.crypto.password.PasswordEncoder;
9
10import static org.junit.Assert.assertEquals;
11import static org.junit.Assert.assertNull;
12import static org.mockito.Matchers.isA;
13import static org.mockito.Mockito.times;
14import static org.mockito.Mockito.verify;
15import static org.mockito.Mockito.verifyNoMoreInteractions;
16import static org.mockito.Mockito.verifyZeroInteractions;
17import static org.mockito.Mockito.when;
18
19@RunWith(MockitoJUnitRunner.class)
20public class RepositoryUserServiceTest {
21
22 private static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";
23 private static final String REGISTRATION_FIRST_NAME = "John";
24 private static final String REGISTRATION_LAST_NAME = "Smith";
25 private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;
26 private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;
27
28 private RepositoryUserService registrationService;
29
30 @Mock
31 private PasswordEncoder passwordEncoder;
32
33 @Mock
34 private UserRepository repository;
35
36 @Before
37 public void setUp() {
38 registrationService = new RepositoryUserService(passwordEncoder, repository);
39 }
40
41 @Test
42 public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {
43 RegistrationForm registration = newRegistrationViaSocialSignIn();
44
45 when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);
46
47 when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {
48 @Override
49 public User answer(InvocationOnMock invocation) throws Throwable {
50 Object[] arguments = invocation.getArguments();
51 return (User) arguments[0];
52 }
53 });
54
55 User createdUserAccount = registrationService.registerNewUserAccount(registration);
56
57 assertEquals(REGISTRATION_EMAIL_ADDRESS, createdUserAccount.getEmail());
58 assertEquals(REGISTRATION_FIRST_NAME, createdUserAccount.getFirstName());
59 assertEquals(REGISTRATION_LAST_NAME, createdUserAccount.getLastName());
60 assertEquals(SOCIAL_SIGN_IN_PROVIDER, createdUserAccount.getSignInProvider());
61 assertEquals(ROLE_REGISTERED_USER, createdUserAccount.getRole());
62 assertNull(createdUserAccount.getPassword());
63
64 verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);
65 verify(repository, times(1)).save(createdUserAccount);
66 verifyNoMoreInteractions(repository);
67 verifyZeroInteractions(passwordEncoder);
68 }
69
70 private RegistrationForm newRegistrationViaSocialSignIn() {
71 RegistrationForm registration = new RegistrationForm();
72
73 registration.setEmail(REGISTRATION_EMAIL_ADDRESS);
74 registration.setFirstName(REGISTRATION_FIRST_NAME);
75 registration.setLastName(REGISTRATION_LAST_NAME);
76 registration.setSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
77
78 return registration;
79 }
80}
The first factory method has the following consequences:
- The part of our test method, which creates the new RegistrationForm object, is a lot cleaner than before and the name of the factory method describes the state of the created RegistrationForm object.
- The configuration of our mock object is harder to read because the value of the the email property is “hidden” inside our factory method.
- Our assertions are harder to read because the property values of the created RegistrationForm object are “hidden” inside our factory method.
I think that it is fair to say that even though the first factory method has its benefits, it has serious drawbacks as well.
Let’s see if the second factory method can eliminate those drawbacks.
The name of the second factory method is newRegistrationViaSocialSignIn(), and it takes the email address, first name, last name, and social sign in provider as method parameters. After we have added this factory method to our test class, the source of our unit test looks as follows (the relevant parts are highlighted):
1import org.junit.Before;
2import org.junit.Test;
3import org.junit.runner.RunWith;
4import org.mockito.Mock;
5import org.mockito.invocation.InvocationOnMock;
6import org.mockito.runners.MockitoJUnitRunner;
7import org.mockito.stubbing.Answer;
8import org.springframework.security.crypto.password.PasswordEncoder;
9
10import static org.junit.Assert.assertEquals;
11import static org.junit.Assert.assertNull;
12import static org.mockito.Matchers.isA;
13import static org.mockito.Mockito.times;
14import static org.mockito.Mockito.verify;
15import static org.mockito.Mockito.verifyNoMoreInteractions;
16import static org.mockito.Mockito.verifyZeroInteractions;
17import static org.mockito.Mockito.when;
18
19@RunWith(MockitoJUnitRunner.class)
20public class RepositoryUserServiceTest {
21
22 private static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";
23 private static final String REGISTRATION_FIRST_NAME = "John";
24 private static final String REGISTRATION_LAST_NAME = "Smith";
25 private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;
26 private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;
27
28 private RepositoryUserService registrationService;
29
30 @Mock
31 private PasswordEncoder passwordEncoder;
32
33 @Mock
34 private UserRepository repository;
35
36 @Before
37 public void setUp() {
38 registrationService = new RepositoryUserService(passwordEncoder, repository);
39 }
40
41 @Test
42 public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {
43 RegistrationForm registration = newRegistrationViaSocialSignIn(REGISTRATION_EMAIL_ADDRESS,
44 REGISTRATION_FIRST_NAME,
45 REGISTRATION_LAST_NAME,
46 SOCIAL_MEDIA_SERVICE
47 );
48
49 when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);
50
51 when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {
52 @Override
53 public User answer(InvocationOnMock invocation) throws Throwable {
54 Object[] arguments = invocation.getArguments();
55 return (User) arguments[0];
56 }
57 });
58
59 User createdUserAccount = registrationService.registerNewUserAccount(registration);
60
61 assertEquals(REGISTRATION_EMAIL_ADDRESS, createdUserAccount.getEmail());
62 assertEquals(REGISTRATION_FIRST_NAME, createdUserAccount.getFirstName());
63 assertEquals(REGISTRATION_LAST_NAME, createdUserAccount.getLastName());
64 assertEquals(SOCIAL_SIGN_IN_PROVIDER, createdUserAccount.getSignInProvider());
65 assertEquals(ROLE_REGISTERED_USER, createdUserAccount.getRole());
66 assertNull(createdUserAccount.getPassword());
67
68 verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);
69 verify(repository, times(1)).save(createdUserAccount);
70 verifyNoMoreInteractions(repository);
71 verifyZeroInteractions(passwordEncoder);
72 }
73
74 private RegistrationForm newRegistrationViaSocialSignIn(String emailAddress, String firstName, String lastName, SocialMediaService signInProvider) {
75 RegistrationForm registration = new RegistrationForm();
76
77 registration.setEmail(emailAddress);
78 registration.setFirstName(firstName);
79 registration.setLastName(lastName);
80 registration.setSignInProvider(signInProvider);
81
82 return registration;
83 }
84}
The second factory method has the following consequences:
- The part of our test method, which creates the new RegistrationForm object, is a bit messier than the same code which uses the first factory method. However, it is still cleaner than the original code because the name of the factory method describes the state of the created object.
- It seems to eliminate the drawbacks of the first factory method because the property values of the created object are not “hidden” inside the factory method.
Seems cool, right?
It would be really easy to think that all is well in the paradise, but that is not the case. Although we have seen that factory methods can make our tests more readable, the thing is that they are a good choice only when the following conditions are met:
- The factory method doesn’t have too many method parameters. When the number of method parameter grows, our tests become harder to write and read. The obvious question is: how many method parameters a factory method can have? Unfortunately it is hard to give an exact answer to that question but I think that using a factory method is a good choice if the factory method has only a handful of method parameters.
- The test data doesn’t have too much variation. The problem of using factory methods is that a single factory method is typically suitable for one use case. If we need to support N use cases, we need to have N factory methods. This is a problem because over time our factory methods become bloated, messy, and hard to maintain (especially if we use the object mother pattern).
Let’s find out if test data builders can solve some of these problems.
Using Test Data Builders
A test data builder is a class which creates new objects by using the builder pattern. The builder pattern described in Effective Java has many benefits, but our primary motivation is to provide a fluent API for creating the objects used in our tests.
We can create a test data builder class which creates new RegistrationForm objects by following these steps:
- Create a RegistrationFormBuilder class.
- Add a RegistrationForm field to the created class. This field contains a reference to the created object.
- Add a default constructor to the created class and implement it by creating a new RegistrationForm object.
- Add methods which are used to set the property values of the created RegistrationForm object. Each method sets the property value by calling the correct setter method and returns a reference to the RegistrationFormBuilder object. Remember that the method names of these methods can either make or break our DSL.
- Add a build() method to the created class and implement it by returning the created RegistrationForm object.
The source code of our test data builder class looks as follows:
1public class RegistrationFormBuilder {
2
3 private RegistrationForm registration;
4
5 public RegistrationFormBuilder() {
6 registration = new RegistrationForm();
7 }
8
9 public RegistrationFormBuilder email(String email) {
10 registration.setEmail(email);
11 return this;
12 }
13
14 public RegistrationFormBuilder firstName(String firstName) {
15 registration.setFirstName(firstName);
16 return this;
17 }
18
19 public RegistrationFormBuilder lastName(String lastName) {
20 registration.setLastName(lastName);
21 return this;
22 }
23
24 public RegistrationFormBuilder isSocialSignInViaSignInProvider(SocialMediaService signInProvider) {
25 registration.setSignInProvider(signInProvider);
26 return this;
27 }
28
29 public RegistrationForm build() {
30 return registration;
31 }
32}
After we have modified our unit test to use the new test data builder class, its source code looks as follows (The relevant part is highlighted):
1import org.junit.Before;
2import org.junit.Test;
3import org.junit.runner.RunWith;
4import org.mockito.Mock;
5import org.mockito.invocation.InvocationOnMock;
6import org.mockito.runners.MockitoJUnitRunner;
7import org.mockito.stubbing.Answer;
8import org.springframework.security.crypto.password.PasswordEncoder;
9
10import static org.junit.Assert.assertEquals;
11import static org.junit.Assert.assertNull;
12import static org.mockito.Matchers.isA;
13import static org.mockito.Mockito.times;
14import static org.mockito.Mockito.verify;
15import static org.mockito.Mockito.verifyNoMoreInteractions;
16import static org.mockito.Mockito.verifyZeroInteractions;
17import static org.mockito.Mockito.when;
18
19@RunWith(MockitoJUnitRunner.class)
20public class RepositoryUserServiceTest {
21
22 private static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";
23 private static final String REGISTRATION_FIRST_NAME = "John";
24 private static final String REGISTRATION_LAST_NAME = "Smith";
25 private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;
26 private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;
27
28 private RepositoryUserService registrationService;
29
30 @Mock
31 private PasswordEncoder passwordEncoder;
32
33 @Mock
34 private UserRepository repository;
35
36 @Before
37 public void setUp() {
38 registrationService = new RepositoryUserService(passwordEncoder, repository);
39 }
40
41 @Test
42 public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {
43 RegistrationForm registration = new RegistrationFormBuilder()
44 .email(REGISTRATION_EMAIL_ADDRESS)
45 .firstName(REGISTRATION_FIRST_NAME)
46 .lastName(REGISTRATION_LAST_NAME)
47 .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
48 .build();
49
50 when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);
51
52 when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {
53 @Override
54 public User answer(InvocationOnMock invocation) throws Throwable {
55 Object[] arguments = invocation.getArguments();
56 return (User) arguments[0];
57 }
58 });
59
60 User createdUserAccount = registrationService.registerNewUserAccount(registration);
61
62 assertEquals(REGISTRATION_EMAIL_ADDRESS, createdUserAccount.getEmail());
63 assertEquals(REGISTRATION_FIRST_NAME, createdUserAccount.getFirstName());
64 assertEquals(REGISTRATION_LAST_NAME, createdUserAccount.getLastName());
65 assertEquals(SOCIAL_SIGN_IN_PROVIDER, createdUserAccount.getSignInProvider());
66 assertEquals(ROLE_REGISTERED_USER, createdUserAccount.getRole());
67 assertNull(createdUserAccount.getPassword());
68
69 verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);
70 verify(repository, times(1)).save(createdUserAccount);
71 verifyNoMoreInteractions(repository);
72 verifyZeroInteractions(passwordEncoder);
73 }
74}
If you want to learn more about fluent APIs, you should read the following articles:
As we can see, test data builders have the following benefits:
- The code which creates new RegistrationForm objects is both easy to read and write. I am a big fan of fluent APIs, and I think that this code is both beautiful and elegant.
- The builder pattern ensures that the variation found from our test data is no longer a problem because we can simply add new methods to the test data builder class.
- The configuration of our mock object and our assertions are easy to read because the constants are visible in our test method and our DSL emphasizes the meaning of each property value.
So, should we use the builder pattern for everything?
NO!
We should use test data builders only when it makes sense. In other words, we should use them when
- We have set more than a handful of property values.
- Our test data has a lot of variation.
The builder pattern is a perfect choice if one of these conditions is true. The reason for this is that we can create a domain-specific language by naming the setter-like methods of the builder class. This makes our tests easy to read and write even if we would have create a lot of different objects and set a lot of property values.
That is the power of the builder patten.
That is all for today. Let’s move on and summarize what we learned from this blog post.
Summary
We learned why it is a bad idea to create objects in the test method by using the new keyword, and we learned two different ways to create the objects which are used in our tests.
To be more specific, this blog post has taught us three things:
- It is a bad idea to create the required objects in the test method by using the new keyword because it makes our tests messy and hard to read.
- If we have to set only a handful of property values and our test data doesn’t have a lot of variation, we should create the required object by using a factory method.
- If we have to set a lot of property values and / or our test data has a lot of variation, we should create the required object by using a test data builder.