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:

Suddenly everything was clear to me.

My "Test With Spring" course helps you to write unit, integration, and end-to-end tests for Spring and Spring Boot Web Apps: CHECK IT OUT >>

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:

import com.nitorcreations.junit.runners.NestedRunner import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.springframework.security.crypto.password.PasswordEncoder; import static com.googlecode.catchexception.CatchException.catchException; import static com.googlecode.catchexception.CatchException.caughtException; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @RunWith(NestedRunner.class) public class RepositoryUserServiceTest { private RepositoryUserService registrationService; private PasswordEncoder passwordEncoder; private UserRepository repository; @Before public void setUp() { passwordEncoder = mock(PasswordEncoder.class); repository = mock(UserRepository.class); registrationService = new RepositoryUserService(passwordEncoder, repository); } public class RegisterNewUserAccount { private final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com"; private final String REGISTRATION_FIRST_NAME = "John"; private final String REGISTRATION_LAST_NAME = "Smith"; private final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER; public class WhenUserUsesSocialSignIn { private RegistrationForm registration; @Before public void setUp() { RegistrationForm registration = new RegistrationFormBuilder() .email(REGISTRATION_EMAIL_ADDRESS) .firstName(REGISTRATION_FIRST_NAME) .lastName(REGISTRATION_LAST_NAME) .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER) .build(); } public class WhenUserAccountIsFoundWithEmailAddress { @Before public void setUp() { given(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).willReturn(new User()); } @Test public void shouldThrowException() throws DuplicateEmailException { catchException(registrationService).registerNewUserAccount(registration); assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class); } @Test public void shouldNotSaveNewUserAccount() throws DuplicateEmailException { catchException(registrationService).registerNewUserAccount(registration); verify(repository, never()).save(isA(User.class)); } } public class WhenEmailAddressIsUnique { @Before public void setUp() { given(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).willReturn(null); given(repository.save(isA(User.class))).willAnswer(new Answer<User>() { @Override public User answer(InvocationOnMock invocation) throws Throwable { Object[] arguments = invocation.getArguments(); return (User) arguments[0]; } }); } @Test public void shouldSaveNewUserAccount() throws DuplicateEmailException { registrationService.registerNewUserAccount(registration); ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class); verify(repository, times(1)).save(isA(User.class)); } @Test public void shouldSetCorrectEmailAddress() throws DuplicateEmailException { registrationService.registerNewUserAccount(registration); ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class); verify(repository, times(1)).save(userAccountArgument.capture()); User createdUserAccount = userAccountArgument.getValue(); assertThatUser(createdUserAccount) .hasEmail(REGISTRATION_EMAIL_ADDRESS); } @Test public void shouldSetCorrectFirstAndLastName() throws DuplicateEmailException { registrationService.registerNewUserAccount(registration); ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class); verify(repository, times(1)).save(userAccountArgument.capture()); User createdUserAccount = userAccountArgument.getValue(); assertThatUser(createdUserAccount) .hasFirstName(REGISTRATION_FIRST_NAME) .hasLastName(REGISTRATION_LAST_NAME) } @Test public void shouldCreateRegisteredUser() throws DuplicateEmailException { registrationService.registerNewUserAccount(registration); ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class); verify(repository, times(1)).save(userAccountArgument.capture()); User createdUserAccount = userAccountArgument.getValue(); assertThatUser(createdUserAccount) .isRegisteredUser() } @Test public void shouldSetSignInProvider() throws DuplicateEmailException { registrationService.registerNewUserAccount(registration); ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class); verify(repository, times(1)).save(userAccountArgument.capture()); User createdUserAccount = userAccountArgument.getValue(); assertThatUser(createdUserAccount) .isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER); } @Test public void shouldNotCreateEncodedPasswordForUser() throws DuplicateEmailException { registrationService.registerNewUserAccount(registration); verifyZeroInteractions(passwordEncoder); } @Test public void shouldReturnCreatedUserAccount() throws DuplicateEmailException { User returnedUserAccount = registrationService.registerNewUserAccount(registration); ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class); verify(repository, times(1)).save(userAccountArgument.capture()); User createdUserAccount = userAccountArgument.getValue(); assertThat(returnedUserAccount) .isEqualTo(createdUserAccount); } } } } }

you should read my blog post: If you don’t understand how the nested configuration works,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):

catchException(registrationService).registerNewUserAccount(registration); assertThat(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:

ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class); verify(repository, times(1)).save(userAccountArgument.capture()); User 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:

testCompile ( 'org.assertj:assertj-core:3.2.0', 'info.solidsoft.mockito:mockito-java8:0.3.0' )

Second, if we use Maven, we have to add the following dependency declarations into our pom.xml file:

<dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.2.0</version> <scope>test</scope> </dependency> <dependency> <groupId>info.solidsoft.mockito</groupId> <artifactId>mockito-java8</artifactId> <version>0.3.0</version> <scope>test</scope> </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:

catchException(registrationService).registerNewUserAccount(registration); assertThat(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:

Throwable t = catchThrowable(() -> registrationService.registerNewUserAccount(registration)); assertThat(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:

assertThatThrownBy(() -> registrationService.registerNewUserAccount(registration)) .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:

ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class); verify(repository, times(1)).save(userAccountArgument.capture()); User createdUserAccount = userAccountArgument.getValue(); assertThatUser(createdUserAccount) .hasFirstName(REGISTRATION_FIRST_NAME) .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:

verify(repository, times(1)).save(assertArg( createdUserAccount -> assertThatUser(createdUserAccount) .hasFirstName(REGISTRATION_FIRST_NAME) .hasLastName(REGISTRATION_LAST_NAME) ));

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):

import com.nitorcreations.junit.runners.NestedRunner import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.springframework.security.crypto.password.PasswordEncoder; import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @RunWith(NestedRunner.class) public class RepositoryUserServiceTest { private RepositoryUserService registrationService; private PasswordEncoder passwordEncoder; private UserRepository repository; @Before public void setUp() { passwordEncoder = mock(PasswordEncoder.class); repository = mock(UserRepository.class); registrationService = new RepositoryUserService(passwordEncoder, repository); } public class RegisterNewUserAccount { private final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com"; private final String REGISTRATION_FIRST_NAME = "John"; private final String REGISTRATION_LAST_NAME = "Smith"; private final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER; public class WhenUserUsesSocialSignIn { private RegistrationForm registration; @Before public void setUp() { RegistrationForm registration = new RegistrationFormBuilder() .email(REGISTRATION_EMAIL_ADDRESS) .firstName(REGISTRATION_FIRST_NAME) .lastName(REGISTRATION_LAST_NAME) .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER) .build(); } public class WhenUserAccountIsFoundWithEmailAddress { @Before public void setUp() { given(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).willReturn(new User()); } @Test public void shouldThrowException() throws DuplicateEmailException { assertThatThrownBy(() -> registrationService.registerNewUserAccount(registration)) .isExactlyInstanceOf(DuplicateEmailException.class); } @Test public void shouldNotSaveNewUserAccount() throws DuplicateEmailException { catchThrowable(() -> registrationService.registerNewUserAccount(registration)); verify(repository, never()).save(isA(User.class)); } } public class WhenEmailAddressIsUnique { @Before public void setUp() { given(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).willReturn(null); given(repository.save(isA(User.class))).willAnswer(new Answer<User>() { @Override public User answer(InvocationOnMock invocation) throws Throwable { Object[] arguments = invocation.getArguments(); return (User) arguments[0]; } }); } @Test public void shouldSaveNewUserAccount() throws DuplicateEmailException { registrationService.registerNewUserAccount(registration); ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class); verify(repository, times(1)).save(isA(User.class)); } @Test public void shouldSetCorrectEmailAddress() throws DuplicateEmailException { registrationService.registerNewUserAccount(registration); verify(repository, times(1)).save(assertArg( createdUserAccount -> assertThatUser(createdUserAccount) .hasEmail(REGISTRATION_EMAIL_ADDRESS); )); } @Test public void shouldSetCorrectFirstAndLastName() throws DuplicateEmailException { registrationService.registerNewUserAccount(registration); verify(repository, times(1)).save(assertArg( createdUserAccount -> assertThatUser(createdUserAccount) .hasFirstName(REGISTRATION_FIRST_NAME) .hasLastName(REGISTRATION_LAST_NAME) )); } @Test public void shouldCreateRegisteredUser() throws DuplicateEmailException { registrationService.registerNewUserAccount(registration); verify(repository, times(1)).save(assertArg( createdUserAccount -> assertThatUser(createdUserAccount) .isRegisteredUser() )); } @Test public void shouldSetSignInProvider() throws DuplicateEmailException { registrationService.registerNewUserAccount(registration); verify(repository, times(1)).save(assertArg( createdUserAccount -> assertThatUser(createdUserAccount) .isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER); )); } @Test public void shouldNotCreateEncodedPasswordForUser() throws DuplicateEmailException { registrationService.registerNewUserAccount(registration); verifyZeroInteractions(passwordEncoder); } @Test public void shouldReturnCreatedUserAccount() throws DuplicateEmailException { User returnedUserAccount = registrationService.registerNewUserAccount(registration); verify(repository, times(1)).save(assertArg( createdUserAccount -> assertThat(returnedUserAccount) .isEqualTo(createdUserAccount); )); } } } } }

Let’s summarize what learned from this blog post.

My "Test With Spring" course helps you to write unit, integration, and end-to-end tests for Spring and Spring Boot Web Apps: CHECK IT OUT >>

Summary

This blog post has taught us two things: