By Ashley Waldron
All of the source code for this series can be found in this repository. Specifically, the code used throughout this item can be found here and its corresponding test classes found here.
Remove the bloat
This one’s an easy one. We’re nearly there when it comes to having a nice neatly written test that works and is maintainable. There’s just one more step to do and it’s an easy one. We just need to stop creating the mockUserEntity and createUserProfileRequest objects in test methods. This cuts down on code bloat and is redundant since we have the @BeforeEach functionality of JUnit to allow us to write this once and have it execute on every test.
You can do this easily in IntelliJ by highlighting the code block ā right-click ā Refactor ā Extract Method as shown in the following screenshot. Or even better, just highlight the code and hit CTRL+ALT+M in windows.

This will extract out the code into a separate method that you can now reuse. Make sure to add in the first line of the method below (96) since we need to instantiate the mockSavedUserEntity to a new object each time (it’s much safer to start with a fresh instance for each test). Previously it was instantiated once at the class level where it was declared but you can remove that instantiation now if you want.
private void createTestUserEntity() {
mockSavedUserEntity = new UserEntity();
mockSavedUserEntity.setEmployeeNumber(TEST_EMPLOYEE_NUMBER);
mockSavedUserEntity.setRegion(TEST_REGION);
mockSavedUserEntity.setEmailAddress(TEST_EMAIL_ADDRESS);
mockSavedUserEntity.setId(TEST_UUID);
}Now do the same for the createUserProfileRequest object and your code should probably look like this:
@Test
void createSuccessItem5a() {
given(userRepository.save(userEntityArgumentCaptor.capture())).willReturn(mockSavedUserEntity);
given(employeeNumberGenerator.generate(TEST_REGION)).willReturn(TEST_EMPLOYEE_NUMBER);
CreateUserProfileRequest createUserProfileRequest = createTestCreateUserProfileRequest();
CreateUserProfileResponse createUserProfileResponse = item5UserService.create(createUserProfileRequest);
assertThat(createUserProfileResponse.getEmployeeNumber(), is(equalTo(TEST_EMPLOYEE_NUMBER)));
assertThat(createUserProfileResponse.getEmailAddress(), is(equalTo(TEST_EMAIL_ADDRESS)));
assertThat(createUserProfileResponse.getRegion(), is(equalTo(TEST_REGION)));
assertThat(createUserProfileResponse.getId(), is(equalTo(TEST_UUID)));
assertThat(userEntityArgumentCaptor.getValue().getEmployeeNumber(), is(equalTo(TEST_EMPLOYEE_NUMBER)));
assertThat(userEntityArgumentCaptor.getValue().getEmailAddress(), is(equalTo(TEST_EMAIL_ADDRESS)));
assertThat(userEntityArgumentCaptor.getValue().getRegion(), is(equalTo(TEST_REGION)));
assertThat(userEntityArgumentCaptor.getValue().getId(), matchesPattern(UUID_PATTERN));
then(eventBroadcaster).should().broadcast(EventType.USER_REGISTRATION, mockSavedUserEntity);
then(emailService).should().registerSubscriber(mockSavedUserEntity);
}
private CreateUserProfileRequest createTestCreateUserProfileRequest() {
createUserProfileRequest = new CreateUserProfileRequest();
createUserProfileRequest.setEmailAddress(TEST_EMAIL_ADDRESS);
createUserProfileRequest.setRegion(TEST_REGION);
createUserProfileRequest.shouldReceiveMarketingEmails(true);
return createUserProfileRequest;
}
private void createTestUserEntity() {
mockSavedUserEntity = new UserEntity();
mockSavedUserEntity.setEmployeeNumber(TEST_EMPLOYEE_NUMBER);
mockSavedUserEntity.setRegion(TEST_REGION);
mockSavedUserEntity.setEmailAddress(TEST_EMAIL_ADDRESS);
mockSavedUserEntity.setId(TEST_UUID);
}Almost there but line 69 is redundant. We should just make the createUserProfileRequest a class level field like the mockSavedUserEntity. So let’s do that and make the new createTestCreateUserProfileRequest() method return void.
So now the code looks like this:
.............
private UserEntity mockSavedUserEntity;
private CreateUserProfileRequest createUserProfileRequest;
@Test
void createSuccessItem5a() {
given(userRepository.save(userEntityArgumentCaptor.capture())).willReturn(mockSavedUserEntity);
given(employeeNumberGenerator.generate(TEST_REGION)).willReturn(TEST_EMPLOYEE_NUMBER);
CreateUserProfileResponse createUserProfileResponse = item5UserService.create(createUserProfileRequest);
assertThat(createUserProfileResponse.getEmployeeNumber(), is(equalTo(TEST_EMPLOYEE_NUMBER)));
assertThat(createUserProfileResponse.getEmailAddress(), is(equalTo(TEST_EMAIL_ADDRESS)));
assertThat(createUserProfileResponse.getRegion(), is(equalTo(TEST_REGION)));
assertThat(createUserProfileResponse.getId(), is(equalTo(TEST_UUID)));
assertThat(userEntityArgumentCaptor.getValue().getEmployeeNumber(), is(equalTo(TEST_EMPLOYEE_NUMBER)));
assertThat(userEntityArgumentCaptor.getValue().getEmailAddress(), is(equalTo(TEST_EMAIL_ADDRESS)));
assertThat(userEntityArgumentCaptor.getValue().getRegion(), is(equalTo(TEST_REGION)));
assertThat(userEntityArgumentCaptor.getValue().getId(), matchesPattern(UUID_PATTERN));
then(eventBroadcaster).should().broadcast(EventType.USER_REGISTRATION, mockSavedUserEntity);
then(emailService).should().registerSubscriber(mockSavedUserEntity);
}
private void createTestCreateUserProfileRequest() {
createUserProfileRequest = new CreateUserProfileRequest();
createUserProfileRequest.setEmailAddress(TEST_EMAIL_ADDRESS);
createUserProfileRequest.setRegion(TEST_REGION);
createUserProfileRequest.shouldReceiveMarketingEmails(true);
}
private void createTestUserEntity() {
mockSavedUserEntity = new UserEntity();
mockSavedUserEntity.setEmployeeNumber(TEST_EMPLOYEE_NUMBER);
mockSavedUserEntity.setRegion(TEST_REGION);
mockSavedUserEntity.setEmailAddress(TEST_EMAIL_ADDRESS);
mockSavedUserEntity.setId(TEST_UUID);
}Now let’s make the test suite run these methods at the start of every test by adding the following @BeforeEach method:
@BeforeEach
void setup() {
createTestUserEntity();
createTestCreateUserProfileRequest();
}Use dedicated test setup and test data classes
We’re almost there. One of the things about testing a large application is that many classes will deal with the same object types. In our example the UserService works with UserEntity objects. But we can see that so too do the EventBroadcaster and MarketingEmailService classes. These 2 classes will have unit tests of their own which will also need to setup test UserEntity objects. If we keep this setup code here then the unit tests for those classes will also need this same boiler plate test object setup code. Plus, they probably aren’t the only classes which deal with UserEntity objects. Now think about how many different object types you have in your code for which this is also true.
The best practice here is just to create a new class called TestDataCreator (or whatever) and move the setup methods above in there so they can be reused everywhere in the code that needs to setup these objects. If you find this class getting too large you can split it into separate classes (e.g. UserEntityTestDataCreator and CreateUserProfileRequestTestDataCreator) and keep the relevant methods in each one. Not a bad idea to do this from the start to be honest. But you can make that decision yourself.
So let’s extract these 2 methods (and make them public and static) to a separate class, along with all of the constants (which we now also have to make public) [SeeĀ TestDataCreator]:
package com.effective.unit.tests.service.item5;
import com.effective.unit.tests.domain.CreateUserProfileRequest;
import com.effective.unit.tests.domain.UserEntity;
import java.util.UUID;
public class TestDataCreator {
public static final String UUID_PATTERN = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$";
public static final String TEST_EMPLOYEE_NUMBER = "EUS1002";
public static final String TEST_REGION = "US-EAST";
public static final String TEST_EMAIL_ADDRESS = "TestEmailAddress@test.com";
public static final String TEST_UUID = UUID.randomUUID().toString();
public static CreateUserProfileRequest createTestCreateUserProfileRequest() {
CreateUserProfileRequest createUserProfileRequest = new CreateUserProfileRequest();
createUserProfileRequest.setEmailAddress(TEST_EMAIL_ADDRESS);
createUserProfileRequest.setRegion(TEST_REGION);
createUserProfileRequest.shouldReceiveMarketingEmails(true);
return createUserProfileRequest;
}
public static UserEntity createTestUserEntity() {
UserEntity mockSavedUserEntity = new UserEntity();
mockSavedUserEntity.setEmployeeNumber(TEST_EMPLOYEE_NUMBER);
mockSavedUserEntity.setRegion(TEST_REGION);
mockSavedUserEntity.setEmailAddress(TEST_EMAIL_ADDRESS);
mockSavedUserEntity.setId(TEST_UUID);
return mockSavedUserEntity;
}
}And finally, update our test code to now look like this [SeeĀ Item5UserServiceTest]:
package com.effective.unit.tests.service.item5;
import com.effective.unit.tests.domain.CreateUserProfileRequest;
import com.effective.unit.tests.domain.CreateUserProfileResponse;
import com.effective.unit.tests.domain.UserEntity;
import com.effective.unit.tests.domain.enums.EventType;
import com.effective.unit.tests.repositories.UserRepository;
import com.effective.unit.tests.service.EmployeeNumberGenerator;
import com.effective.unit.tests.service.EventBroadcaster;
import com.effective.unit.tests.service.MarketingEmailService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static com.effective.unit.tests.service.item5.TestDataCreator.TEST_EMAIL_ADDRESS;
import static com.effective.unit.tests.service.item5.TestDataCreator.TEST_EMPLOYEE_NUMBER;
import static com.effective.unit.tests.service.item5.TestDataCreator.TEST_REGION;
import static com.effective.unit.tests.service.item5.TestDataCreator.TEST_UUID;
import static com.effective.unit.tests.service.item5.TestDataCreator.UUID_PATTERN;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.matchesPattern;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
@ExtendWith(MockitoExtension.class)
public class Item5UserServiceTest {
@InjectMocks
private Item5UserService item5UserService;
@Mock
private UserRepository userRepository;
@Mock
private EventBroadcaster eventBroadcaster;
@Mock
private MarketingEmailService emailService;
@Mock
private EmployeeNumberGenerator employeeNumberGenerator;
@Captor
private ArgumentCaptor<UserEntity> userEntityArgumentCaptor;
private UserEntity mockSavedUserEntity;
private CreateUserProfileRequest createUserProfileRequest;
@BeforeEach
void setup() {
mockSavedUserEntity = TestDataCreator.createTestUserEntity();
createUserProfileRequest = TestDataCreator.createTestCreateUserProfileRequest();
}
@Test
void createSuccessItem5a() {
given(userRepository.save(userEntityArgumentCaptor.capture())).willReturn(mockSavedUserEntity);
given(employeeNumberGenerator.generate(TEST_REGION)).willReturn(TEST_EMPLOYEE_NUMBER);
CreateUserProfileResponse createUserProfileResponse = item5UserService.create(createUserProfileRequest);
assertThat(createUserProfileResponse.getEmployeeNumber(), is(equalTo(TEST_EMPLOYEE_NUMBER)));
assertThat(createUserProfileResponse.getEmailAddress(), is(equalTo(TEST_EMAIL_ADDRESS)));
assertThat(createUserProfileResponse.getRegion(), is(equalTo(TEST_REGION)));
assertThat(createUserProfileResponse.getId(), is(equalTo(TEST_UUID)));
assertThat(userEntityArgumentCaptor.getValue().getEmployeeNumber(), is(equalTo(TEST_EMPLOYEE_NUMBER)));
assertThat(userEntityArgumentCaptor.getValue().getEmailAddress(), is(equalTo(TEST_EMAIL_ADDRESS)));
assertThat(userEntityArgumentCaptor.getValue().getRegion(), is(equalTo(TEST_REGION)));
assertThat(userEntityArgumentCaptor.getValue().getId(), matchesPattern(UUID_PATTERN));
then(eventBroadcaster).should().broadcast(EventType.USER_REGISTRATION, mockSavedUserEntity);
then(emailService).should().registerSubscriber(mockSavedUserEntity);
}
}At the end of this item, we’ve now shaved a decent amount of bloat off the testcase and set ourselves up for removing a lot of potential bloat all around the entire test suite.
Birth of an Effective Unit Test
After 5 items I think we can finally say that we have an effective unit test. And the funny thing is, if you compare the total test code written here in item 5 to the first unit test we wrote back in item 1, it’s hardly any more effort at all! It really isn’t. There are a few extra lines of code (as to be expected) but knowing the correct approaches and conventions to use make the test very close to absolutely bulletproof (and easier to maintain) at almost no extra expense. If almost any logic changes in this default happy path test then something will fail and let you know. This is what we want.
For any developers who don’t yet have a lot of experience writing unit tests, these first 5 items are the most fundamentally important in writing effective unit tests. Most other things fall into place naturally enough after this.