I released the starter package of my Test With Spring course. Take a look at the course >>

Writing Clean Tests – Java 8 to the Rescue

running silhouettes

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.

I released the starter package of my "Test With Spring" course:

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

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.

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:

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:

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.

I released the starter package of my "Test With Spring" course:

CHECK IT OUT >>

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.

About the Author

Petri Kainulainen is passionate about software development and continuous improvement. He is specialized in software development with the Spring Framework and is the author of Spring Data book.

About Petri Kainulainen →

15 comments… add one
  • Nice article but i have a question.

    Let’s suppose we do have a method that does some field validations before send to the repository and it throws a IllegalArgumentException if one of required fields was not set correcttly. ( email, firstName, lastName.. etc.. ). We should test every message as well with hasMessage(message) method.

    So we decide to follow this approach to avoid boilerplate code ( set every field necessary to reach that poit of code that we want to test ) and you realize that we now have another big problem. A lot of inner classes cause we want to reuse every piece of code made before reach that condition. ( is this a big problem ? )

    What do you think about this ?

    Thanks

    Reply
    • Hi Marcelo,

      Thank you for your kind words. I will answer to your question below:

      Let’s suppose we do have a method that does some field validations before send to the repository and it throws a IllegalArgumentException if one of required fields was not set correcttly. ( email, firstName, lastName.. etc.. ). We should test every message as well with hasMessage(message) method.

      I assume that this validation logic ensures that we cannot create a User object that has invalid information (i.e. it defends our code from programming errors). If this is the case, typically I put this logic to the User class. The reason why I mentioned this is that even though we should definitely test this logic, we don’t necessarily want to test it in a service test.

      We could also test this in the unit test which tests the methods of the User class. The benefit of this approach is that we can write very focused unit tests that tests the validation logic without unnecessary setup code. This makes our tests easier to read and maintain.

      Of course, if the logic is related to a specific business required, or we have to provide a sensible error message to the user, we should definitely test it in a service test.

      So we decide to follow this approach to avoid boilerplate code ( set every field necessary to reach that poit of code that we want to test ) and you realize that we now have another big problem. A lot of inner classes cause we want to reuse every piece of code made before reach that condition. ( is this a big problem ? )

      It can be a problem. For example, if you have a deep inner class hierarchy, you might notice an exception which states that filename of your test class is too long. However, I have noticed this problem only twice and I have written hundreds (closer to one thousand) of unit tests that use this approach.

      Even though solving that technical problem is rather easy, there is another (and a lot more dangerous problem):

      If our class hierarchy doesn’t make sense, our tests are hard to read and maintain.

      In other words, we should pay close attention to the structure of our inner class hierarchy and to the naming of our inner classes. Also, if we notice a problem, we should fix it immediately before it escalates into a bigger problem.

      I think that creating a proper nested hierarchy is definitely more challenging than using the “traditional flat hierarchy”, but it is a lot more rewarding as well.

      Reply
      • Good points Petri

        First time i did something like this was very frustrating and i knew there were something wrong but i could not find where / what was wrong
        I agree when you say that the validation should be in another place rather than in the service layer but i can not see where to put this since it does make part of the business. ( i really would like to do this )

        I will keep trying this cause the idea is amazing.

        Thanks !

        Reply
        • Hi Marcelo,

          Remember that you don’t have to go all-in right away. For example, you can “practice” by writing simple unit tests. After you think that you can design a good inner class hierarchy for your test classes, you can move on and write a few “complex” unit tests.

          If it feels natural to you, keep on going. If not, you can always practice a bit more.

          Reply
    • Hello Petri!

      I hope you are doing great! Thanks to your tutorials the testing is going terrific! Many thanks!
      Surfing on the web I found out about validating JSON schemas for REST services, but seems very tedious work!. Would like to know what do you think about it

      Thanks!

      Reply
  • Hi Petri,

    By any chance do you have this code in a github repository?, I’m concerned about how to integrate this within a spring boot project in regards of what file structure to use and standard general configuration.

    Thank you very much for your valuable tutorials!

    Reply
    • Hi Jose,

      By any chance do you have this code in a github repository?

      No, but the example applications of my Spring Data JPA tutorial have unit tests that use the approach described in this blog post. You can get these example applications from Github (you can find them from the criteria-api, querydsl, and query-methods directories).

      If you have any additional questions, don’t hesitate to ask them.

      Reply
      • Hi Petri,

        Thank you for your response. I’ve been following your tutorials and last samples sent.
        Although I’ve come across with probably a conceptual doubt.
        In base of the following scenario, if in the real service I start giving a random string as response this test will always pass.
        Isn’t this approach too manipulated?.
        Thank you very much for your time.

        
        @Before
        public void setUp(){
        	given(messageBoardService.getAllInfo()).willReturn("{}");
        }
        
        @Test
        public void shouldReturnResponseAsJson() throws Exception {
        	mockMvc.perform(get("/v1/message/getall"))
        		andExpect(content().contentType(TestConstants.APPLICATION_JSON));
        }
        
        
        Reply
        • Hi Jose,

          In base of the following scenario, if in the real service I start giving a random string as response this test will always pass.

          This test will indeed pass. However, the other unit tests, which ensure that your controller method returns the correct Json document, should fail. Also, the unit tests that test the getAllInfo() method should fail.

          If you have written integration (or end-to-end) tests for this function, they should fail as well.

          You should remember that if you isolate the external dependencies of the tested unit by using test doubles, you cannot catch mistakes that are made outside the tested unit. That is why you need to write integration and end-to-end tests as well.

          Did this answer to your question?

          Reply
          • Hi Petri,

            Yes, now it starts to make sense to me. I’ll keep working on this!
            Many, many thanks!

          • You are welcome!

Leave a Comment