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:

  1. AssertJ core 3.0.0 for Java 8 was released.
  2. 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 don’t understand how the nested configuration works, you should read my blog post: Writing Clean Tests - Small Is Beautiful.

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.

We can use Java 8 lambdas for this purpose, but it leads into the same problem.

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:

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.