Writing Clean Tests - Small Is Beautiful

We have learned that "clean" unit tests might not be as clean as we think.

We have done our best to make our unit tests as clean as possible. Our tests are formatted properly, use domain-specific language, and avoid excessive mocking.

Nevertheless, our unit tests are not clean because:

  • When we make changes to the tested code, most of our existing unit tests don't compile or fail when we run them. Fixing these unit tests is slow and frustrating.
  • When we add new methods to the tested class, we realize that writing new unit tests is a lot slower than it should be.

If this is the case, it is very likely that our unit tests suffer from these common problems:

  • The method names of our test methods are way too long. If a test fails, the method name doesn’t necessarily describe what went wrong. Also, it is hard to get a brief overview of the situations that are covered by our tests. This means that we might test the same situation more than once.
  • Our test methods contains duplicate code that configures mock objects and creates other objects that are used in our tests. This means that our tests are hard to read, write, and maintain.
  • Because there is no clean way to share common configuration with only a few test methods, we must put all constants to the beginning of the test class. Some of you might claim that this is a minor issue, and you are right, but it still makes our test classes messier than they should be.

Let's find out how we can solve all of these problems.

Nested Configuration to the Rescue

If we want to fix the problems found from our unit tests, we have to

  • Describe the tested method and the state under test in a way that doesn't require long method names.
  • Find a way to move the common configuration from test methods to setup methods.
  • Create a common context for test methods, and make setup methods and constants visible only to the test methods that belong to the created context.

There is a JUnit runner that can help us to achieve these goals. It is called the NestedRunner, and it allows us to run test methods placed in nested inner classes.

Before we can start solve our problems by using NestedRunner, we have to add the NestedRunner dependency to our Maven build and ensure that our test methods are invoked by the NestedRunner class.

First, we need to add the following dependency declaration to our pom.xml file:

<dependency>
	<groupId>com.nitorcreations</groupId>
	<artifactId>junit-runners</artifactId>
	<version>1.2</version>
	<scope>test</scope>
</dependency>

Second, we need to make the following changes to the RepositoryUserServiceTest class:

  1. Ensure that the test methods found from the RepositoryUserServiceTest class are invoked by the NestedRunner class.
  2. Remove the @Mock annotations from the passwordEncoder and repository fields.
  3. Create the required mock objects by invoking the static mock() method of the Mockito class and insert them to the passwordEncoder and repository fields.
You can also leave the @Mock annotations to the passwordEncoder and repository fields, and create the mock objects by invoking the static initMocks() method of the MockitoAnnotations class. I decided to use the manual approach here because it is a bit more straightforward.

The source code of the RepositoryUserServiceTest class looks as follows:

import com.nitorcreations.junit.runners.NestedRunner
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.security.crypto.password.PasswordEncoder;
 
import static org.mockito.Mockito.mock;
 
@RunWith(NestedRunner.class)
public class RepositoryUserServiceTest {
 
    private static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";
    private static final String REGISTRATION_FIRST_NAME = "John";
    private static final String REGISTRATION_LAST_NAME = "Smith";
    private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;
 
    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);
    }
}

We have now configured NestedRunner and can start solving the problems found from our unit tests. Let’s start by replacing long method names with a nested class hierarchy.

Replacing Long Test Method Names With a Nested Class Hierarchy

Before we can replace the long test method names with a nested class hierarchy, we need to figure out what situations are covered by our unit tests. If we take a look at our test class, we notice that the unit tests found from the RepositoryUserServiceTest class ensure that:

  • If there already is a user account that has the same email address, our code should
    • throw an exception.
    • not save a new user account.
  • If there is no user account that has the same email address, our code should
    • Save a new user account.
    • Set the correct email address.
    • Set the correct first and last names.
    • Should create a registered user.
    • Set the correct sign in provider.
    • Not create encoded password for the user.
    • Return the the created user account.

We can now eliminate the long test method names by replacing our test methods with a BDD style class hierarchy. The idea is that we:

  1. Create one inner class per tested method. This class can contain a setup method, tests methods, and other inner classes. In our case, the name of this inner class is RegisterNewUserAccount.
  2. Create the class hierarchy that describes the state under test. We can do this by adding inner classes to the RegisterNewUserAccount class (and to its inner classes). We can name these inner classes by using the following syntax: When[StateUnderTest]. We can add this class hierarchy to our test class by following these steps:
    1. Because the user is registering a user account by using social sign in, we have to add the WhenUserUsesSocialSignIn class to the RegisterNewUserAccount class.
    2. Because we have to cover two different situations, we have to add two inner classes (WhenUserAccountIsFoundWithEmailAddress and WhenEmailAddressIsUnique) to the WhenUserUsesSocialSignIn class.
  3. Add the actual test methods to the correct inner classes. Because the class hierarchy describes the tested method and the state under test, the name of each unit test must only describe the expected behavior. One way to do this is to name each test method by using the prefix: should.

After we have created the class hierarchy, the source code of our test class 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.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;
import static org.mockito.Mockito.when;
 
@RunWith(NestedRunner.class)
public class RepositoryUserServiceTest {
 
    private static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";
    private static final String REGISTRATION_FIRST_NAME = "John";
    private static final String REGISTRATION_LAST_NAME = "Smith";
    private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;
 
    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 {
	
		public class WhenUserUsesSocialSignIn {
			
			public class WhenUserAccountIsFoundWithEmailAddress {
			    
				@Test
			    public void shouldThrowException() throws DuplicateEmailException {
			        RegistrationForm registration = new RegistrationFormBuilder()
			                .email(REGISTRATION_EMAIL_ADDRESS)
			                .firstName(REGISTRATION_FIRST_NAME)
			                .lastName(REGISTRATION_LAST_NAME)
			                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
			                .build();
 
			        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(new User());
 
			        catchException(registrationService).registerNewUserAccount(registration);
 
			        assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);
			    }
				
			    @Test
			    public void shouldNotSaveNewUserAccount() throws DuplicateEmailException {
			        RegistrationForm registration = new RegistrationFormBuilder()
			                .email(REGISTRATION_EMAIL_ADDRESS)
			                .firstName(REGISTRATION_FIRST_NAME)
			                .lastName(REGISTRATION_LAST_NAME)
			                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
			                .build();
 
			        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(new User());
 
			        catchException(registrationService).registerNewUserAccount(registration);
 
			        verify(repository, never()).save(isA(User.class));
			    }
			}
			
			public class WhenEmailAddressIsUnique {
			    
				@Test
			    public void shouldSaveNewUserAccount() throws DuplicateEmailException {
			        RegistrationForm registration = new RegistrationFormBuilder()
			                .email(REGISTRATION_EMAIL_ADDRESS)
			                .firstName(REGISTRATION_FIRST_NAME)
			                .lastName(REGISTRATION_LAST_NAME)
			                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
			                .build();
 
			        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);
 
			        registrationService.registerNewUserAccount(registration);
 
			        ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
			        verify(repository, times(1)).save(isA(User.class));
			    }
				
			    @Test
			    public void shouldSetCorrectEmailAddress() throws DuplicateEmailException {
			        RegistrationForm registration = new RegistrationFormBuilder()
			                .email(REGISTRATION_EMAIL_ADDRESS)
			                .firstName(REGISTRATION_FIRST_NAME)
			                .lastName(REGISTRATION_LAST_NAME)
			                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
			                .build();
 
			        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);
 
			        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 {
			        RegistrationForm registration = new RegistrationFormBuilder()
			                .email(REGISTRATION_EMAIL_ADDRESS)
			                .firstName(REGISTRATION_FIRST_NAME)
			                .lastName(REGISTRATION_LAST_NAME)
			                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
			                .build();
 
			        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);
 
			        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 {
			        RegistrationForm registration = new RegistrationFormBuilder()
			                .email(REGISTRATION_EMAIL_ADDRESS)
			                .firstName(REGISTRATION_FIRST_NAME)
			                .lastName(REGISTRATION_LAST_NAME)
			                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
			                .build();
 
			        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);
 
			        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 {
			        RegistrationForm registration = new RegistrationFormBuilder()
			                .email(REGISTRATION_EMAIL_ADDRESS)
			                .firstName(REGISTRATION_FIRST_NAME)
			                .lastName(REGISTRATION_LAST_NAME)
			                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
			                .build();
 
			        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);
 
			        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 {
			        RegistrationForm registration = new RegistrationFormBuilder()
			                .email(REGISTRATION_EMAIL_ADDRESS)
			                .firstName(REGISTRATION_FIRST_NAME)
			                .lastName(REGISTRATION_LAST_NAME)
			                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
			                .build();
 
			        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);
 
			        registrationService.registerNewUserAccount(registration);
 
			        verifyZeroInteractions(passwordEncoder);
			    }
				
			    @Test
			    public void shouldReturnCreatedUserAccount() throws DuplicateEmailException {
			        RegistrationForm registration = new RegistrationFormBuilder()
			                .email(REGISTRATION_EMAIL_ADDRESS)
			                .firstName(REGISTRATION_FIRST_NAME)
			                .lastName(REGISTRATION_LAST_NAME)
			                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
			                .build();
 
			        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);
			        when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {
			            @Override
			            public User answer(InvocationOnMock invocation) throws Throwable {
			                Object[] arguments = invocation.getArguments();
			                return (User) arguments[0];
			            }
			        });
 
			        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);
			    }
			}
		
		}
	}
}

We have now replaced the long test method names with a nested class hierarchy, but the downside of this solution is that we added a lot of duplicate code. Let’s get rid of that code.

Removing Duplicate Code

We can remove all duplicate code from our test class by moving it to the setup methods that are placed to the “correct” inner classes. Before we can identify the “correct” inner classes, we have to understand the execution order of setup and test methods. The best way to understand this is to use a simple example:

@RunWith(NestedRunner.class)
public class TestClass {

	/**
	 * This setup method is invoked before the test and setup methods
	 * found from the inner classes of this class. 
	 
	 * This is a good place for configuration that is shared by all 
	 * test methods found from this test class.
	 */
	@Before
	public void setUpTestClass() {}
	
	public class MethodA {
	
		/**
		 * This setup method is invoked before the test methods found from
		 * this class and before the test and setup methods found from the
		 * inner classes of this class.
		 *
		 * This is a good place for configuration that is shared by all test
		 * methods which ensure that the methodA() is working correctly.
		 */
		@Before
		public void setUpMethodA() {}
		
		@Test
		public void shouldFooBar() {}
		
		public class WhenFoo {
		
			/**
			 * This setup method is invoked before the test methods found from
			 * this class and before the test and setup methods found from the
			 * inner classes of this class.
			 *
			 * This is a good place for configuration which ensures that the methodA()
			 * working correctly when foo is 'true'.
			 */
			@Before
			public void setUpWhenFoo() {}
			
			@Test
			public void shouldBar() {}
		}
		
		public class WhenBar {
		
			@Test
			public shouldFoo() {}
		}
	}
}

In other words, before a test method is invoked, NestedRunner invokes the setup methods by navigating to the test method from the root class of the class hierarchy and invoking all setup methods. Let’s go through the test methods found from our example:

  • Before the test method shouldFooBar() is invoked, NestedRunner invokes the setUpTestClass() and setUpMethodA() methods.
  • Before the test method shouldBar() is invoked, NestedRunner invokes the setUpTestClass(), setUpMethodA(), and setUpWhenFoo() methods.
  • Before the test method shouldFoo() is invoked, NestedRunner invokes the setUpTestClass() and setUpMethodA() methods.

We can now make the necessary modifications to the RepositoryUserServiceTest class by following these steps:

  1. Add a setUp() method to the WhenUserUsesSocialSignIn class and implement it by creating a new RegistrationForm object. This is the right place to do this because all unit tests give a RegistrationForm object as an input to the tested method.
  2. Add a setUp() method to the WhenUserAccountIsFoundWithEmailAddress class and configure our repository mock to return a User object when its findByEmail() method is invoked by using the email address that was entered to the registration form. This is the right place for this code because every unit test that is found from the WhenUserAccountIsFoundWithEmailAddress class assumes that the email address given during the registration is not unique.
  3. Add a setUp() method to the WhenEmailAddressIsUnique class and configure our repository mock to 1) return null when its findByEmail() method is invoked by using the email address that was entered to the registration form and 2) return the User object given as a method parameter when its save() method is invoked. This is the right place for this code because every every unit test that is found from the WhenEmailAddressIsUnique class assumes that the email address given during the registration is unique and that the information of the created user account is returned.

After we have done these changes, the source code of our test class 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 static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";
    private static final String REGISTRATION_FIRST_NAME = "John";
    private static final String REGISTRATION_LAST_NAME = "Smith";
    private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;
 
    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 {
	
		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);
			    }
			}
		
		}
	}
}
Notes:

  • This test class uses BDDMockito, but you can use the "standard" Mockito API as well.
  • Our test class does still have some duplicate code that is used to capture the User object given as a method parameter to the save() method of the UserRepository. I left this code to the test methods because I didn't want to make "too many" changes to the original source code. This code should be moved to a private method that is named properly.

Our test class is looking pretty clean, but we can still make it a little cleaner. Let's find out how we can do that.

Linking Constants With the Test Methods

One problem that we face when we replace magic numbers with constants is that we have to add these constants to the beginning of our test class. This means that it is hard to link these constants with the test cases that use them.

If we take look at our unit test class, we notice that we use constants when we create a new RegistrationForm object. Because this happens in the setUp() method of the RegisterNewUserAccount class, we can solve our problem by moving the constants from the beginning of the RepositoryUserServiceTest class to beginning of the RegisterNewUserAccount class.

After we have do this, our test class 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);
			    }
			}
		
		}
	}
}

It is now clear that these constants are relevant for the unit tests that are found from the RegisterNewUserAccount inner class and from its inner classes. This might seem like a small tweak, but I have noticed that small things can make a huge difference.

Let’s move on and summarize what we learned from this blog post.

Summary

This blog post has taught us that

  • We can replace long method names with a BDD style class hierarchy.
  • We can remove duplicate code by moving that code to setup methods and putting these methods to the correct inner classes.
  • We can link the constants with test cases that use them by declaring the constants in the correct inner class.

Update: Some redditors argue that this solution is not cleaner than the old one. I agree that the new unit tests look very different than "regular" JUnit tests, and it can be hard to read them (at first).

However, if you use IntelliJ IDEA, working with unit tests becomes a pleasure. Let's take a look at few screenshots (if you want to see the full size image, click the thumbnail):

We can "close" the inner classes that don't interest us and concentrate on the interesting test cases:

ideatestclass

When we run our unit tests, we can navigate the test class hierarchy by using the IDEA's Test Runner tab:

ideatestrunner

In other words, if we use IDEA and NestedRunner, it is very easy to figure out the requirements of the tested method. I think that this is a huge improvement over the "traditional" way (a long list of test methods which have long and inaccurate method names).

P.S. I recommend that you read a blog post titled: Three Steps to Code Quality via TDD. It is an excellent blog post and you can use its lessons even if you don’t use TDD.

33 comments… add one
  • Kostadin Golev Apr 9, 2015 @ 14:08

    Very nice!

    I have been looking for something like this for a long time. JUnit really needed the ideas behind RSpec (for Ruby) and Jasmine (for JavaScript).

    Thanks for sharing and for the guide!

    • Petri Apr 9, 2015 @ 14:24

      You are welcome! I am happy to hear that this tutorial was useful to you.

      JUnit really needed the ideas behind RSpec (for Ruby) and Jasmine (for JavaScript).

      I agree. On the other hand, if you really want to see how deep the rabbit hole goes, you can write your tests with Spock Framework. I haven't done this myself, but I plan to give it a shot in the future.

      • Kostadin Golev Apr 11, 2015 @ 21:37

        Yes, spock is on my todo list as well. Another interesting item there is spec2 for Scala: http://etorreborre.github.io/specs2/

        Issue with both is the same - its a different language. OK, at least its JVM, but that makes it harder to use them when working with other people.

        On the other hand both can be a good way to get into groovy/Scala :)

        • Petri Apr 12, 2015 @ 9:37

          Specs2 looks interesting. It reminds me of JBehave, but its syntax is (of course) a bit less verbose.

          I agree that it can be hard to motivate some people to use different programming language for tests. However, I have noticed that sometimes people can really surprise you. Especially if you are suggesting an improvement that makes their job easier (I assume that this is the case with Spock and Specs2).

  • Shaun Finglas Apr 9, 2015 @ 22:30

    Nice post, thanks for the recommendation. I'm glad the "Three Steps..." post proved useful :)

    • Petri Apr 9, 2015 @ 22:53

      You are welcome. I really enjoyed reading that post (or should I say posts) :)

  • Jacob Zimmerman Apr 14, 2015 @ 18:36

    I like it. I didn't think I would at the beginning, but its benefits are quite clear.

    I'm just curious: have you tested to see how much of a performance hit this does? It surely has some performance hindrance. While it's really nice for all the reasons you said, I don't want to be picking up something that is too much of a performance hit.

    • Petri Apr 14, 2015 @ 21:50

      I’m just curious: have you tested to see how much of a performance hit this does?

      This is a very important question. I have converted some old unit tests to use NestedRunner and I haven't noticed any performance problems. However, I don't trust my gut feeling. I will run some performance tests during the weekend and report the results here.

      • Petri Apr 18, 2015 @ 18:01

        I did some "non-scientific" performance testing by using the Maven Profiler.

        I analyzed the unit tests of my Spring Data JPA example application.

        The master branch has 102 unit tests, and most of them use NestedRunner. There are 8 unit tests that don't use it.

        This is the last commit that doesn't use NestedRunner at all. It has 92 unit tests (including the 8 unit test that don't use NestedRunner in the master branch).

        I ran this command: mvn clean test -Dprofile five times for both versions and measured how many seconds it takes to finish the test phase. I got the following results:

        • NestedRunner (102 unit tests): 2.826s,2.485s,2.444s,2.539s,2.549s
        • Normal (92 unit tests): 2.572s,2.375s,2.334s,2.461s,2.413s

        It seems that using NestedRunner might have a small negative effect to the performance of your test suite. Of course this test is not very useful because:

        • The number of unit tests is not equal.
        • There is no real warm up phase.

        Nevertheless, the test results indicate that using NestedRunner does not have a catastrophic negative effect to the performance of your test suite.

        • Jacob Zimmerman Jun 10, 2015 @ 4:02

          That's good to know. I believe I will be switching over to these NestedRunners when I have the chance.

          • Petri Jun 10, 2015 @ 19:17

            Sounds good.

            Also, you don't necessarily have to go all-in right away. I am in the middle of a project that has tests which use NestedRunner and tests which don't use it. Every time when I need to make changes to my tests, I rewrite the tests to use NestedRunner, and eventually all of my tests will use NestedRunner.

  • Guillaume Apr 14, 2015 @ 20:31

    Very interesting read, I've been looking for something like this for a while. By any chance, do you know if it there's a mechanism that would allow @RunWith(NestedRunner.class) to be combined with @RunWith(SpringJUnit4ClassRunner.class)? It would be great if we could use NestedRunner to do some integration tests.

    • Petri Apr 14, 2015 @ 21:09

      As far I know, it is not possible to combine the functionality of NestedRunner and SpringJUnit4ClassRunner. This is a shame because it would make this library even more useful.

      • Jacob Zimmerman Apr 17, 2015 @ 4:36

        Yeah, they maybe should have designed Runners to be decorators. I don't know if this is possible, but it should would have been nice :)

        • Jacob Zimmerman Apr 17, 2015 @ 4:37

          sure* would have been

        • Petri Apr 17, 2015 @ 9:26

          They do have a runner called WrappingParametrizedRunner. It supports a scenario where you need to combine @RunWith(Parameterized.class) with @RunWith(SomethingElse).

          Maybe it's possible to borrow some ideas from this runner and implement a new runner which makes it possible to combine @RunWith(NestedRunner.class) with @RunWith(SpringJUnit4ClassRunner.class).

  • Guillaume May 20, 2015 @ 4:45

    Just saw this today:

    https://github.com/spring-projects/spring-framework/commit/d1b1c4f888b5ab5c60cf4beb87f577143bdbebe7

    Looks like ClassRule support for Spring integration tests is coming in Spring 4.2, this should make it easier to write those nested tests.

    • Petri May 20, 2015 @ 9:44

      That looks awesome. It seems that it is a good thing that I haven't started updating my Spring MVC Test tutorial yet :) Thank you for sharing this information.

  • Anonymous Dec 5, 2015 @ 11:11

    the one downside I see from this method is, you lose the ability to run single tests in IntelliJ. You need to execute all test from one class to make them run. Do you have any solution to this?

    • Petri Dec 5, 2015 @ 12:02

      You are right. Also, the test runner tab of the latest IntelliJ Idea version (15) isn't as clear as it used to be :(

      Unfortunately I don't have a "clean" solution to this problem. When you need to run only a one unit test, you can either ignore all other tests found from your test class or create a new test class that has only one unit test.

      I know that this is a bit cumbersome, but since I have done this only a few times after I wrote this blog post, it hasn't been a big problem to me. Your experiences might (of course) be totally different, and I think that this might prevent you from using this method.

      • Kostadin Golev Jun 3, 2016 @ 11:21

        I solved the test runner tab in IDEA just by switching to HierarchicalContextRunner.

        All you need to do is add the dependency and change the @RunWith:
        http://mvnrepository.com/artifact/de.bechte.junit/junit-hierarchicalcontextrunner/4.11.1
        @RunWith(HierarchicalContextRunner.class)

        No other changes needed, easy! Functionality is exactly the same.

        Can't wait to get my hands on JUnit5 at work, where there is "native" support for nesting via annotation, so no need to use runners at all

        • Petri Jun 4, 2016 @ 11:38

          Hi Kostadin,

          That is interesting. I will give it a shot right away. Thank you for the tip.

          I am also waiting for JUnit 5, but it seems that I have to wait until they release the first stable version. I hope that it happens during this year.

  • Kaidjin Apr 11, 2016 @ 18:17

    Unfortunately, it seems that the NestedRunner doesn't work with SpringClassRule and SpringMethodRule. Neither does ContextHierarchicalRunner.

    I was really hoping to be able to cmobine the two, but it seems the only way is to split use cases into multiple tests, or to abandon the spring context (or continue not using hierarchies of course).

    • Petri Apr 15, 2016 @ 19:32

      That is indeed unfortunate :(

      At the moment I am putting all integration tests which test a specific function into the same test class. The problem of this approach is that the method names become quite long (and it is still hard to describe the tested scenario). I was actually hoping that I could use those rules with NestedRunner, but I didn't have time to test it myself. It is sad to hear that it is not possible :(

    • Antti Aug 25, 2016 @ 13:42

      Workaround to get integration tests work with HierarchicalContext/Nested -Runner is to have the root context's @Before to prepare the Spring context with:
      new TestContextManager(getClass()).prepareTestInstance(this);
      The only drawback to this seemed to be that @Transactional doesn't work as intended, but you can always work around it with truncating the affected tables.

      • Petri Sep 5, 2016 @ 21:01

        Cool. I will test this myself. Thank you for sharing this tip!

Leave a Reply