Unit Testing with Spring Boot

Table Of Contents

Writing good unit tests can be considered an art that is hard to master. But the good news is that the mechanics supporting it are easy to learn.

This tutorial provides you with these mechanics and goes into the technical details that are necessary to write good unit tests with a focus on Spring Boot applications.

We’ll have a look at how to create Spring beans in a testable manner and then discuss usage of Mockito and AssertJ, both libraries that Spring Boot by default includes for testing.

Note that this article only discusses unit tests. Integration tests, tests of the web layer and tests of the persistence layer will be discussed in upcoming articles of this series.

Example Code

This article is accompanied by a working code example on GitHub.

The “Testing with Spring Boot” Series

This tutorial is part of a series:

  1. Unit Testing with Spring Boot
  2. Testing Spring MVC Web Controllers with Spring Boot and @WebMvcTest
  3. Testing JPA Queries with Spring Boot and @DataJpaTest
  4. Integration Tests with @SpringBootTest

If you like learning from videos, make sure to check out Philip’s Testing Spring Boot Applications Masterclass (if you buy through this link, I get a cut).

Dependencies

For the unit test in this tutorial, we’ll use JUnit Jupiter (JUnit 5), Mockito, and AssertJ. We’ll also include Lombok to reduce a bit of boilerplate code:

dependencies{
  compileOnly('org.projectlombok:lombok')
  testCompile('org.springframework.boot:spring-boot-starter-test')
  testCompile 'org.junit.jupiter:junit-jupiter-engine:5.2.0'
  testCompile('org.mockito:mockito-junit-jupiter:2.23.0')
}

Mockito and AssertJ are automatically imported with the spring-boot-starter-test dependency, but we’ll have to include Lombok ourselves.

Don’t Use Spring in Unit Tests

If you have written tests with Spring or Spring Boot in the past, you’ll probably say that we don’t need Spring to write unit tests. Why is that?

Consider the following “unit” test that tests a single method of the RegisterUseCase class:

@ExtendWith(SpringExtension.class)
@SpringBootTest
class RegisterUseCaseTest {

  @Autowired
  private RegisterUseCase registerUseCase;

  @Test
  void savedUserHasRegistrationDate() {
    User user = new User("zaphod", "zaphod@mail.com");
    User savedUser = registerUseCase.registerUser(user);
    assertThat(savedUser.getRegistrationDate()).isNotNull();
  }

}

This test takes about 4.5 seconds to run on an empty Spring project on my computer.

But a good unit test only takes milliseconds. Otherwise it hinders the “test / code / test” flow promoted by the idea of Test-Driven Development (TDD). But even when we’re not practicing TDD, waiting on a test that takes too long ruins our concentration.

Execution of the test method above actually only takes milliseconds. The rest of the 4.5 seconds is due to the @SpringBootRun telling Spring Boot to set up a whole Spring Boot application context.

So we have started the whole application only to autowire a RegisterUseCase instance into our test. It will take even longer once the application gets bigger and Spring has to load more and more beans into the application context.

So, why this article when we shouldn’t use Spring Boot in a unit test? To be honest, most of this tutorial is about writing unit tests without Spring Boot.

Creating a Testable Spring Bean

However, there are some things we can do to make our Spring beans better testable.

Field Injection is Evil

Let’s start with a bad example. Consider the following class:

@Service
public class RegisterUseCase {

  @Autowired
  private UserRepository userRepository;

  public User registerUser(User user) {
    return userRepository.save(user);
  }

}

This class cannot be unit tested without Spring because it provides no way to pass in a UserRepository instance. Instead, we need to write the test in the way discussed in the previous section to let Spring create a UserRepository instance and inject it into the field annotated with @Autowired.

The lesson here is not to use field injection.

Providing a Constructor

Actually let’s not use the @Autowired annotation at all:

@Service
public class RegisterUseCase {

  private final UserRepository userRepository;

  public RegisterUseCase(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  public User registerUser(User user) {
    return userRepository.save(user);
  }

}

This version allows constructor injection by providing a constructor that allows to pass in a UserRepository instance. In the unit test, we can now create such an instance (perhaps a mock instance as we’ll discuss later) and pass it into the constructor.

Spring will automatically use this constructor to instantiate a RegisterUseCase object when creating the production application context. Note that prior to Spring 5, we need to add the @Autowired annotation to the constructor for Spring to find the constructor.

Also note that the UserRepository field is now final. This makes sense, since the field content won’t ever change during the lifetime of an application. It also helps to avoid programming errors, because the compiler will complain if we have forgotten to initialize the field.

Reducing Boilerplate Code

Using Lombok’s @RequiredArgsConstructor annotation we can let the constructor be automatically generated:

@Service
@RequiredArgsConstructor
public class RegisterUseCase {

  private final UserRepository userRepository;

  public User registerUser(User user) {
    user.setRegistrationDate(LocalDateTime.now());
    return userRepository.save(user);
  }

}

Now, we have a very concise class without boilerplate code that can be instantiated easily in a plain java test case:

class RegisterUseCaseTest {

  private UserRepository userRepository = ...;

  private RegisterUseCase registerUseCase;

  @BeforeEach
  void initUseCase() {
    registerUseCase = new RegisterUseCase(userRepository);
  }

  @Test
  void savedUserHasRegistrationDate() {
    User user = new User("zaphod", "zaphod@mail.com");
    User savedUser = registerUseCase.registerUser(user);
    assertThat(savedUser.getRegistrationDate()).isNotNull();
  }

}

There’s a piece missing, yet, and that is how to mock away the UserRepository instance our class under test depends on, because we don’t want to rely on the real thing, which probably needs a connection to a database.

Using Mockito to Mock Dependencies

The de-facto standard mocking library nowadays is Mockito. It provides at least two ways to create a mocked UserRepository to fill the blank in the previous code example.

Mocking Dependencies with Plain Mockito

The first way is to just use Mockito programmatically:

private UserRepository userRepository = Mockito.mock(UserRepository.class);

This will create an object that looks like a UserRepository from the outside. By default, it will do nothing when a method is called and return null if the method has a return value.

Our test would now fail with a NullPointerException at assertThat(savedUser.getRegistrationDate()).isNotNull() because userRepository.save(user) now returns null.

So, we have to tell Mockito to return something when userRepository.save() is called. We do this with the static when method:

@Test
void savedUserHasRegistrationDate() {
  User user = new User("zaphod", "zaphod@mail.com");
  when(userRepository.save(any(User.class))).then(returnsFirstArg());
  User savedUser = registerUseCase.registerUser(user);
  assertThat(savedUser.getRegistrationDate()).isNotNull();
}

This will make userRepository.save() return the same user object that is passed into the method.

Mockito has a whole lot more features that allow for mocking, matching arguments and verifying method calls. For more information have a look at the reference documentation.

Mocking Dependencies with Mockito’s @Mock Annotation

An alternative way of creating mock objects is Mockito’s @Mock annotation in combination with the MockitoExtension for JUnit Jupiter:

@ExtendWith(MockitoExtension.class)
class RegisterUseCaseTest {

  @Mock
  private UserRepository userRepository;

  private RegisterUseCase registerUseCase;

  @BeforeEach
  void initUseCase() {
    registerUseCase = new RegisterUseCase(userRepository);
  }

  @Test
  void savedUserHasRegistrationDate() {
    // ...
  }

}

The @Mock annotation specifies the fields in which Mockito should inject mock objects. The @MockitoExtension tells Mockito to evaluate those @Mock annotations because JUnit does not do this automatically.

The result is the same as if calling Mockito.mock() manually, it’s a matter of taste which way to use. Note, though, that by using MockitoExtension our tests are bound to the test framework.

Note that instead of constructing an RegisterUseCase object manually, we can just as well use the @InjectMocks annotation on the registerUseCase field. Mockito will then create an instance for us, following a specified algorithm:

@ExtendWith(MockitoExtension.class)
class RegisterUseCaseTest {

  @Mock
  private UserRepository userRepository;

  @InjectMocks
  private RegisterUseCase registerUseCase;

  @Test
  void savedUserHasRegistrationDate() {
    // ...
  }

}

Creating Readable Assertions with AssertJ

Another library that comes automatically with the Spring Boot test support is AssertJ. We have already used it above to implement our assertion:

assertThat(savedUser.getRegistrationDate()).isNotNull();

However, wouldn’t it be nice to make the assertion even more readable? Like this, for example:

assertThat(savedUser).hasRegistrationDate();

There are many cases where small changes like this make the test so much better to understand. So, let’s create our own custom assertion in the test sources folder:

class UserAssert extends AbstractAssert<UserAssert, User> {

  UserAssert(User user) {
    super(user, UserAssert.class);
  }

  static UserAssert assertThat(User actual) {
    return new UserAssert(actual);
  }

  UserAssert hasRegistrationDate() {
    isNotNull();
    if (actual.getRegistrationDate() == null) {
      failWithMessage(
        "Expected user to have a registration date, but it was null"
      );
    }
    return this;
  }
}

Now, if we import the assertThat method from the new UserAssert class instead from the AssertJ library, we can use the new, easier to read assertion.

Creating a custom assertion like this may seem like a lot of work, but it’s actually done in a couple minutes. I believe strongly that it’s worth to invest these minutes to create readable test code, even if it’s only marginally better readable afterwards. We only write the test code once, after all, and others (including “future me”) have to read, understand and then manipulate the code many, many times during the lifetime of the software.

If it still seems like too much work, have a look at AssertJ’s Assertions Generator.

Conclusion

There are reasons to start up a Spring application in a test, but for plain unit tests, it’s not necessary. It’s even harmful due to the longer turnaround times. Instead, we should build our Spring beans in a way that easily supports writing plain unit tests for.

The Spring Boot Test Starter comes with Mockito and AssertJ as testing libraries.

Let’s exploit those testing libraries to create expressive unit tests!

The code example in its final form is available on github.

If you like learning from videos, make sure to check out Philip’s Testing Spring Boot Applications Masterclass (if you buy through this link, I get a cut).

Written By:

Tom Hombergs

Written By:

Tom Hombergs

As a professional software engineer, consultant, architect, general problem solver, I've been practicing the software craft for more than fifteen years and I'm still learning something new every day. I love sharing the things I learned, so you (and future me) can get a head start. That's why I founded reflectoring.io.

Recent Posts

Guide to JUnit 5 Functional Interfaces

In this article, we will get familiar with JUnit 5 functional interfaces. JUnit 5 significantly advanced from its predecessors. Features like functional interfaces can greatly simplify our work once we grasp their functionality.

Read more

Getting Started with Spring Security and JWT

Spring Security provides a comprehensive set of security features for Java applications, covering authentication, authorization, session management, and protection against common security threats such as CSRF (Cross-Site Request Forgery).

Read more

Creating and Publishing an NPM Package with Automated Versioning and Deployment

In this step-by-step guide, we’ll create, publish, and manage an NPM package using TypeScript for better code readability and scalability. We’ll write test cases with Jest and automate our NPM package versioning and publishing process using Changesets and GitHub Actions.

Read more