In the previous part of this tutorial we solved some common problems found from “clean” unit tests by using nested configuration.
I was very happy with the final test class, but after a while I realized that something was bothering me. The only problem was that I couldn’t figure out what it was.
I ignored that feeling and continued writing unit tests. Then two things happened:
- AssertJ core 3.0.0 for Java 8 was released.
- I read a blog post titled: More compact Mockito with Java 8, lambda expressions and Mockito-Java8 add-ons.
Suddenly everything was clear to me.
Revealing the Hidden Problems
Although we have made several small improvements to our test class, it still has two problems.
Before we will take a closer look at these problems, let’s refresh our memory and take a look at the source code of our test class. It looks as follows:
1import com.nitorcreations.junit.runners.NestedRunner
2import org.junit.Before;
3import org.junit.Test;
4import org.junit.runner.RunWith;
5import org.mockito.ArgumentCaptor;
6import org.mockito.Mock;
7import org.mockito.invocation.InvocationOnMock;
8import org.mockito.runners.MockitoJUnitRunner;
9import org.mockito.stubbing.Answer;
10import org.springframework.security.crypto.password.PasswordEncoder;
11
12import static com.googlecode.catchexception.CatchException.catchException;
13import static com.googlecode.catchexception.CatchException.caughtException;
14import static org.assertj.core.api.Assertions.assertThat;
15import static org.mockito.BDDMockito.given;
16import static org.mockito.Matchers.isA;
17import static org.mockito.Mockito.mock;
18import static org.mockito.Mockito.never;
19import static org.mockito.Mockito.times;
20import static org.mockito.Mockito.verify;
21import static org.mockito.Mockito.verifyZeroInteractions;
22
23@RunWith(NestedRunner.class)
24public class RepositoryUserServiceTest {
25
26 private RepositoryUserService registrationService;
27
28 private PasswordEncoder passwordEncoder;
29
30 private UserRepository repository;
31
32 @Before
33 public void setUp() {
34 passwordEncoder = mock(PasswordEncoder.class);
35 repository = mock(UserRepository.class);
36
37 registrationService = new RepositoryUserService(passwordEncoder, repository);
38 }
39
40 public class RegisterNewUserAccount {
41
42 private final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";
43 private final String REGISTRATION_FIRST_NAME = "John";
44 private final String REGISTRATION_LAST_NAME = "Smith";
45 private final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;
46
47 public class WhenUserUsesSocialSignIn {
48
49 private RegistrationForm registration;
50
51 @Before
52 public void setUp() {
53 RegistrationForm registration = new RegistrationFormBuilder()
54 .email(REGISTRATION_EMAIL_ADDRESS)
55 .firstName(REGISTRATION_FIRST_NAME)
56 .lastName(REGISTRATION_LAST_NAME)
57 .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
58 .build();
59 }
60
61 public class WhenUserAccountIsFoundWithEmailAddress {
62
63 @Before
64 public void setUp() {
65 given(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).willReturn(new User());
66 }
67
68 @Test
69 public void shouldThrowException() throws DuplicateEmailException {
70 catchException(registrationService).registerNewUserAccount(registration);
71 assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);
72 }
73
74 @Test
75 public void shouldNotSaveNewUserAccount() throws DuplicateEmailException {
76 catchException(registrationService).registerNewUserAccount(registration);
77 verify(repository, never()).save(isA(User.class));
78 }
79 }
80
81 public class WhenEmailAddressIsUnique {
82
83 @Before
84 public void setUp() {
85 given(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).willReturn(null);
86
87 given(repository.save(isA(User.class))).willAnswer(new Answer<User>() {
88 @Override
89 public User answer(InvocationOnMock invocation) throws Throwable {
90 Object[] arguments = invocation.getArguments();
91 return (User) arguments[0];
92 }
93 });
94 }
95
96 @Test
97 public void shouldSaveNewUserAccount() throws DuplicateEmailException {
98 registrationService.registerNewUserAccount(registration);
99
100 ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
101 verify(repository, times(1)).save(isA(User.class));
102 }
103
104 @Test
105 public void shouldSetCorrectEmailAddress() throws DuplicateEmailException {
106 registrationService.registerNewUserAccount(registration);
107
108 ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
109 verify(repository, times(1)).save(userAccountArgument.capture());
110 User createdUserAccount = userAccountArgument.getValue();
111
112 assertThatUser(createdUserAccount)
113 .hasEmail(REGISTRATION_EMAIL_ADDRESS);
114 }
115
116 @Test
117 public void shouldSetCorrectFirstAndLastName() throws DuplicateEmailException {
118 registrationService.registerNewUserAccount(registration);
119
120 ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
121 verify(repository, times(1)).save(userAccountArgument.capture());
122 User createdUserAccount = userAccountArgument.getValue();
123
124 assertThatUser(createdUserAccount)
125 .hasFirstName(REGISTRATION_FIRST_NAME)
126 .hasLastName(REGISTRATION_LAST_NAME)
127 }
128
129 @Test
130 public void shouldCreateRegisteredUser() throws DuplicateEmailException {
131 registrationService.registerNewUserAccount(registration);
132
133 ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
134 verify(repository, times(1)).save(userAccountArgument.capture());
135 User createdUserAccount = userAccountArgument.getValue();
136
137 assertThatUser(createdUserAccount)
138 .isRegisteredUser()
139 }
140
141 @Test
142 public void shouldSetSignInProvider() throws DuplicateEmailException {
143 registrationService.registerNewUserAccount(registration);
144
145 ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
146 verify(repository, times(1)).save(userAccountArgument.capture());
147 User createdUserAccount = userAccountArgument.getValue();
148
149 assertThatUser(createdUserAccount)
150 .isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
151 }
152
153 @Test
154 public void shouldNotCreateEncodedPasswordForUser() throws DuplicateEmailException {
155 registrationService.registerNewUserAccount(registration);
156
157 verifyZeroInteractions(passwordEncoder);
158 }
159
160 @Test
161 public void shouldReturnCreatedUserAccount() throws DuplicateEmailException {
162 User returnedUserAccount = registrationService.registerNewUserAccount(registration);
163
164 ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
165 verify(repository, times(1)).save(userAccountArgument.capture());
166 User createdUserAccount = userAccountArgument.getValue();
167
168 assertThat(returnedUserAccount)
169 .isEqualTo(createdUserAccount);
170 }
171 }
172
173 }
174 }
175}
If you didn’t find any problems from our test code, you should not feel bad about it. It is extremely hard to notice these problems if you don’t know what to look for.
The biggest reason for this is that before Java 8 was released, there was no other way to write these tests. However, after Java 8 was released, testing tools started to take advantage of its features. This means that we can make our tests a bit better.
The two problems found from our test class are:
First, some test methods use the catch-exception library for catching exceptions thrown by the tested code. The problem of this approach is this:
If we want to write assertions for the exception thrown by the tested code, we have to capture it first.
The code that captures the thrown exception and ensures that it is an instance of the DuplicateEmailException class looks as follows (the unnecessary step is highlighted):
1catchException(registrationService).registerNewUserAccount(registration);
2assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);
Obviously this doesn’t look like a huge problem because our test class has only one method that uses this code.
However, if we would be writing tests for a real-life application, the odds are that we would have to write many tests that catch exceptions thrown by the tested code. I agree that it still isn’t a major issue, but if we can make it better, it would be stupid to not do it.
Second, because we have to ensure that the created user account contains the correct information, some test methods need to capture the method parameter that is passed to the save() method of the UserRepository mock. The code that captures the method parameter and gets a reference to the persisted User object looks as follows:
1ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
2verify(repository, times(1)).save(userAccountArgument.capture());
3User createdUserAccount = userAccountArgument.getValue();
The problem is that we have write the same code every time when we want to access the persisted User object. For example, even though our test class is relatively simple, we have to write this code five times. Can you guess how many times we have to do this when we are writing tests for a real-life application?
Exactly. That is why this is a big problem.
Fixing Problems With Java 8
We can fix these problems by using the following libraries:
- AssertJ Core 3.2.0. We use it because it provides a Java 8 friendly way to write assertions for the exceptions thrown by the tested code, but it has a lot of other cool features as well.
- Mockito-Java8 makes mocking more compact by leveraging Java 8 and lambda expressions.
Let’s start by getting the required dependencies.
Getting the Required Dependencies
Before we can fix the problems found from our test class, we need to get the AssertJ Core 3.1.0 and Mockito-Java8 0.3.0 libraries.
First, if we use Gradle, we have to add the following dependency declarations into our build.gradle file:
1testCompile (
2 'org.assertj:assertj-core:3.2.0',
3 'info.solidsoft.mockito:mockito-java8:0.3.0'
4)
Second, if we use Maven, we have to add the following dependency declarations into our pom.xml file:
1<dependency>
2 <groupId>org.assertj</groupId>
3 <artifactId>assertj-core</artifactId>
4 <version>3.2.0</version>
5 <scope>test</scope>
6</dependency>
7<dependency>
8 <groupId>info.solidsoft.mockito</groupId>
9 <artifactId>mockito-java8</artifactId>
10 <version>0.3.0</version>
11 <scope>test</scope>
12</dependency>
Let’s find out how we can catch exceptions without writing any boilerplate code.
Catching Exceptions Without Writing Boilerplate Code
The existing code, which captures the exception thrown by the registerNewUserAccount() method and ensures that it is an instance of the DuplicateEmailException class, looks as follows:
1catchException(registrationService).registerNewUserAccount(registration);
2assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);
If we use AssertJ 3.2.0, we can catch exceptions by using one of these two methods:
First, we can use the static catchThrowable() method of the Assertions class. This method returns the Throwable object that is thrown by the tested code.
The code that captures an exception thrown by the registerNewUserAccount() method looks as follows:
1Throwable t = catchThrowable(() -> registrationService.registerNewUserAccount(registration));
2assertThat(t).isExactlyInstanceOf(DuplicateEmailException.class);
As we can see, this doesn’t really solve our problem. We simply replaced the catch-exception library with AssertJ. Although getting rid of the catch-exception library makes sense if our tests are already using AssertJ, we can do better.
Second, we can use the static assertThatThrownBy() method of the Assertions class. This method returns an AbstractThrowableAssert object that we can use to write assertions for the thrown exception.
The code that captures an exception thrown by the registerNewUserAccount() method looks as follows:
1assertThatThrownBy(() -> registrationService.registerNewUserAccount(registration))
2 .isExactlyInstanceOf(DuplicateEmailException.class);
As we can see, we managed to remove the line that was used to get a reference to the exception thrown by the tested code. It isn’t a huge improvement, but small things add up.
Let’s find out how we can capture method arguments without writing any boilerplate code.
Capturing Method Arguments Without Writing Boilerplate Code
The existing code, which captures the persisted User object and ensures that its first name and last name are correct, looks as follows:
1ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
2verify(repository, times(1)).save(userAccountArgument.capture());
3User createdUserAccount = userAccountArgument.getValue();
4
5assertThatUser(createdUserAccount)
6 .hasFirstName(REGISTRATION_FIRST_NAME)
7 .hasLastName(REGISTRATION_LAST_NAME)
We can capture method arguments with Mockito-Java8 by using the static assertArg() method of the AssertionMatcher class. After we have made the required changes, our new code looks as follows:
1verify(repository, times(1)).save(assertArg(
2 createdUserAccount -> assertThatUser(createdUserAccount)
3 .hasFirstName(REGISTRATION_FIRST_NAME)
4 .hasLastName(REGISTRATION_LAST_NAME)
5));
That looks pretty awesome. We removed two lines of unnecessary code, and created a stronger connection between the expected method invocation and its method parameters. In my opinion, this makes our code look a bit more “natural” and easier to read.
Let’s move on and make these changes to our test class.
What Did We Do?
When we made these changes to our test class, we removed 11 lines of unnecessary code. The source code of our test class looks as follows (the modified parts are highlighted):
1import com.nitorcreations.junit.runners.NestedRunner
2import org.junit.Before;
3import org.junit.Test;
4import org.junit.runner.RunWith;
5import org.mockito.Mock;
6import org.mockito.invocation.InvocationOnMock;
7import org.mockito.runners.MockitoJUnitRunner;
8import org.mockito.stubbing.Answer;
9import org.springframework.security.crypto.password.PasswordEncoder;
10
11import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg;
12import static org.assertj.core.api.Assertions.assertThat;
13import static org.assertj.core.api.Assertions.assertThatThrownBy;
14import static org.assertj.core.api.Assertions.catchThrowable;
15import static org.mockito.BDDMockito.given;
16import static org.mockito.Matchers.isA;
17import static org.mockito.Mockito.mock;
18import static org.mockito.Mockito.never;
19import static org.mockito.Mockito.times;
20import static org.mockito.Mockito.verify;
21import static org.mockito.Mockito.verifyZeroInteractions;
22
23@RunWith(NestedRunner.class)
24public class RepositoryUserServiceTest {
25
26 private RepositoryUserService registrationService;
27
28 private PasswordEncoder passwordEncoder;
29
30 private UserRepository repository;
31
32 @Before
33 public void setUp() {
34 passwordEncoder = mock(PasswordEncoder.class);
35 repository = mock(UserRepository.class);
36
37 registrationService = new RepositoryUserService(passwordEncoder, repository);
38 }
39
40 public class RegisterNewUserAccount {
41
42 private final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";
43 private final String REGISTRATION_FIRST_NAME = "John";
44 private final String REGISTRATION_LAST_NAME = "Smith";
45 private final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;
46
47 public class WhenUserUsesSocialSignIn {
48
49 private RegistrationForm registration;
50
51 @Before
52 public void setUp() {
53 RegistrationForm registration = new RegistrationFormBuilder()
54 .email(REGISTRATION_EMAIL_ADDRESS)
55 .firstName(REGISTRATION_FIRST_NAME)
56 .lastName(REGISTRATION_LAST_NAME)
57 .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
58 .build();
59 }
60
61 public class WhenUserAccountIsFoundWithEmailAddress {
62
63 @Before
64 public void setUp() {
65 given(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).willReturn(new User());
66 }
67
68 @Test
69 public void shouldThrowException() throws DuplicateEmailException {
70 assertThatThrownBy(() -> registrationService.registerNewUserAccount(registration))
71 .isExactlyInstanceOf(DuplicateEmailException.class);
72 }
73
74 @Test
75 public void shouldNotSaveNewUserAccount() throws DuplicateEmailException {
76 catchThrowable(() -> registrationService.registerNewUserAccount(registration));
77 verify(repository, never()).save(isA(User.class));
78 }
79 }
80
81 public class WhenEmailAddressIsUnique {
82
83 @Before
84 public void setUp() {
85 given(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).willReturn(null);
86
87 given(repository.save(isA(User.class))).willAnswer(new Answer<User>() {
88 @Override
89 public User answer(InvocationOnMock invocation) throws Throwable {
90 Object[] arguments = invocation.getArguments();
91 return (User) arguments[0];
92 }
93 });
94 }
95
96 @Test
97 public void shouldSaveNewUserAccount() throws DuplicateEmailException {
98 registrationService.registerNewUserAccount(registration);
99
100 ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
101 verify(repository, times(1)).save(isA(User.class));
102 }
103
104 @Test
105 public void shouldSetCorrectEmailAddress() throws DuplicateEmailException {
106 registrationService.registerNewUserAccount(registration);
107
108 verify(repository, times(1)).save(assertArg(
109 createdUserAccount -> assertThatUser(createdUserAccount)
110 .hasEmail(REGISTRATION_EMAIL_ADDRESS);
111 ));
112 }
113
114 @Test
115 public void shouldSetCorrectFirstAndLastName() throws DuplicateEmailException {
116 registrationService.registerNewUserAccount(registration);
117
118 verify(repository, times(1)).save(assertArg(
119 createdUserAccount -> assertThatUser(createdUserAccount)
120 .hasFirstName(REGISTRATION_FIRST_NAME)
121 .hasLastName(REGISTRATION_LAST_NAME)
122 ));
123 }
124
125 @Test
126 public void shouldCreateRegisteredUser() throws DuplicateEmailException {
127 registrationService.registerNewUserAccount(registration);
128
129 verify(repository, times(1)).save(assertArg(
130 createdUserAccount -> assertThatUser(createdUserAccount)
131 .isRegisteredUser()
132 ));
133 }
134
135 @Test
136 public void shouldSetSignInProvider() throws DuplicateEmailException {
137 registrationService.registerNewUserAccount(registration);
138
139 verify(repository, times(1)).save(assertArg(
140 createdUserAccount -> assertThatUser(createdUserAccount)
141 .isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
142 ));
143 }
144
145 @Test
146 public void shouldNotCreateEncodedPasswordForUser() throws DuplicateEmailException {
147 registrationService.registerNewUserAccount(registration);
148
149 verifyZeroInteractions(passwordEncoder);
150 }
151
152 @Test
153 public void shouldReturnCreatedUserAccount() throws DuplicateEmailException {
154 User returnedUserAccount = registrationService.registerNewUserAccount(registration);
155
156 verify(repository, times(1)).save(assertArg(
157 createdUserAccount -> assertThat(returnedUserAccount)
158 .isEqualTo(createdUserAccount);
159 ));
160 }
161 }
162
163 }
164 }
165}
Let’s summarize what learned from this blog post.
Summary
This blog post has taught us two things:
- We can catch exceptions and write assertions for them without getting a reference to the thrown exception.
- We can capture method arguments and write assertions for them by using lambda expressions.