The devil that is @SpringBootTest!
By: Amir Mohammad Vosough
Tags: Unit Testing, Junit 5, Spring Boot, Integration Testing

I’ve seen a lot of developers claiming one of the “Cool features” of Spring is that it simplifies your unit tests by introducing @SpringBootTest. Now that’s just not true and I think it was not the intention of Spring also! Now let’s first get into what is a “Unit Test” and what is “Not a Unit Test”. I don’t want to be a Word Nazi here, but I have seen this leading to usage of @SpringBootTest on every Unit Test and no one wants that!

To explain the problem, let’s take a step back and see how we [should] test our applications. We have two completely different types of tests on the two ends of a stick, we have “End-To-End Integration Tests” where [hopefully] nothing is mocked and our application will be tested as close as possible to the way it is run in production. Now if these tests passed, then we will say our application is ready! On the other end of the stick, we have “Unit Tests” where one unit of work is tested and everything other than that unit of work is mocked during the test. QAs want to test everything exactly the same way as it is in production (as they should!). But that comes with a cost! Tests will be heavy. Now, us developers want to get a “quicker result” on whether we broke anything by the change we just made or not. So we start “assuming”. We just remove some of the elements of an Integration Test and replace them with something we control. This way we will have more control over the test and we it will be faster also. The more we assume, the more control we have and the faster our tests become. This continues until we reach Unit Tests where we control everything! Basically we will say “assuming this request comes in and DB does that and etc., I expect these to happen”.

For example let’s say I am implementing a “Register User” service method. I will try to make this a bit more real-world to emphasize on how DDD has eased our life here also!

Business expert says when registering a user, I should make sure that the user doesn’t already exist in our DB, then generate an activation link for this user and email the generated activation link to him/her.

Now using DDD, I will translate this to:

I should make sure that the user doesn’t already exist in our User Store, then generate an activation code for this user and send the generated activation code to him/her.

package ir.amv.snippets.unittestdemo;



import java.util.UUID;

import lombok.RequiredArgsConstructor;



/**

* @author Amir

*/

@RequiredArgsConstructor

public class UserService {



private final UserRepository userRepository;

private final ActivationCodeSender activationCodeSender;



public void registerUser(String email) {

userRepository.findByEmail(email)

.ifPresent(user -> {

throw new IllegalArgumentException(String.format("User %s already exists", email));

});

String activationCode = UUID.randomUUID().toString();

activationCodeSender.send(email, activationCode);

userRepository.saveUser(email, activationCode);

}

}

Now, in production, User Store will be our DB (also easily replaceable by an Active Directory User Store thanks to DDD), and sending the activation code will be done by emailing an activation link to the user. This is what happens in Integration Tests. In my Unit Tests however, I can “assume” that the User Store will for example say this user doesn’t already exist. Then expect a generated code to be passed to be sent. I don’t need Spring here. Actually everything other than my UserService should be mocked.

Mockito has some usefull annotations for this. Let’s build my unit test step by step. First we will create our unit test. You should read this as “create mock objects for UserRepository and ActivationCodeSender, and then create an instance of UserService by calling its biggest constructor passing the mocked objects”.

package ir.amv.snippets.unittestdemo;



import static org.junit.jupiter.api.Assertions.*;

import static org.mockito.Mockito.*;



import org.junit.jupiter.api.Nested;

import org.junit.jupiter.api.Test;

import org.junit.jupiter.api.extension.ExtendWith;

import org.mockito.InjectMocks;

import org.mockito.Mock;

import org.mockito.junit.jupiter.MockitoExtension;



@ExtendWith(MockitoExtension.class)

class UserServiceTest {



@Mock

private UserRepository userRepository;



@Mock

private ActivationCodeSender activationCodeSender;



@InjectMocks

private UserService underTest;



}

Now, I want to create Unit Tests for my register method. I will create a Nested class for tests related to this method because in our business case, we have two different scenarios, existing user and none-existing user.

class UserServiceTest { // not shown @Nested

class registerUser {

@Test

void givenExistingUser() {

// given

// when

// then

}



@Test

void givenNoneExistingUser() {

// given

// when

// then

}

}

}

The first case is easy. If user is an existing user, I should get an IllegalArgumentException:

@Test

void givenExistingUser() {

// given

String email = "doesn't really need to be an email because we CONTROL everything!";

when(userRepository.findByEmail(eq(email))).thenReturn(Optional.of(mock(User.class)));

// when & then

assertThrows(IllegalArgumentException.class,

() -> underTest.registerUser(email));

}

And for none-existing user:

@Test

void givenNoneExistingUser() {

// given

String email = "doesn't really need to be an email because we CONTROL everything!";

when(userRepository.findByEmail(eq(email))).thenReturn(Optional.empty());

// when

underTest.registerUser(email);

// then

verify(userRepository).saveUser(eq(email), anyString());

verify(activationCodeSender).send(eq(email), anyString());

}

And that’s it. Now if you like and agree with this way of implementing Unit Tests and are using Intellij IDEA, you can use this template as your default JUnit 5 test class which will come in handy when you Alt+Enter on a class and select “Create Test”: