Unit testing with JUnit 5

JUnit 5 tutorial, part 2: Unit testing Spring MVC with JUnit 5

Unit test a Spring MVC service, controller, and repository with JUnit 5, Mockito, MockMvc, and DBUnit

Tools + toolbelt in a binary environment.
Mihalec / Suebsiri / Getty Images

Spring MVC is one of the most popular Java frameworks for building enterprise Java applications, and it lends itself very well to testing. By design, Spring MVC promotes the separation of concerns and encourages coding against interfaces. These qualities, along with Spring's implementation of dependency injection, make Spring applications very testable.

This tutorial is the second half of my introduction to unit testing with JUnit 5. I'll show you how to integrate JUnit 5 with Spring, then introduce you to three tools that you can use to test Spring MVC controllers, services, and repositories.

download
Download the source code for example applications used in this tutorial. Created by Steven Haines for JavaWorld.

Integrating JUnit 5 with Spring 5

For this tutorial, we are using Maven and Spring Boot, so the first thing that we need to do is add the JUnit 5 dependency to our Maven POM file:


    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.6.0</version>
      <scope>test</scope>
    </dependency>

Just like we did in Part 1, we'll use Mockito for this example. So, we're going to need to add the JUnit 5 Mockito library:


    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-junit-jupiter</artifactId>
      <version>3.2.4</version>
      <scope>test</scope>
    </dependency>
    

@ExtendWith and the SpringExtension class

JUnit 5 defines an extension interface, through which classes can integrate with JUnit tests at various stages of the execution lifecycle. We can enable extensions by adding the @ExtendWith annotation to our test classes and specifying the extension class to load. The extension can then implement various callback interfaces, which will be invoked throughout the test lifecycle: before all tests run, before each test runs, after each test runs, and after all of the tests have run.

Spring defines a SpringExtension class that subscribes to JUnit 5 lifecycle notifications to create and maintain a "test context." Recall that Spring's application context contains all of the Spring beans in an application and that it performs dependency injection to wire together an application and its dependencies. Spring uses the JUnit 5 extension model to maintain the test's application context, which makes writing unit tests with Spring straightforward.

After we've added the JUnit 5 library to our Maven POM file, we can use the SpringExtension.class to extend our JUnit 5 test classes:


@ExtendWith(SpringExtension.class)
class MyTests {
  // ...
}

The example, in this case, is a Spring Boot application. Fortunately the @SpringBootTest annotation already includes the @ExtendWith(SpringExtension.class) annotation, so we only need to include @SpringBootTest.

Adding the Mockito dependency

To properly test each component in isolation and simulate different scenarios, we're going to want to create mock implementations of each class's dependencies. Here is where Mockito comes in. Include the following dependency in your POM file to add support for Mockito:


    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-junit-jupiter</artifactId>
      <version>3.2.4</version>
      <scope>test</scope>
    </dependency>
    

After you've integrated JUnit 5 and Mockito into your Spring application, you can leverage Mockito by simply defining a Spring bean (such as a service or repository) in your test class using the @MockBean annotation. Here's our example:


@SpringBootTest
public class WidgetServiceTest {
    /**
     * Autowire in the service we want to test
     */
    @Autowired
    private WidgetService service;

    /**
     * Create a mock implementation of the WidgetRepository
     */
    @MockBean
    private WidgetRepository repository;
    ...
}

In this example, we're creating a mock WidgetRepository inside our WidgetServiceTest class. When Spring sees this, it will automatically wire it into our WidgetService so that we can create different scenarios in our test methods. Each test method will configure the behavior of the WidgetRepository, such as by returning the requested Widget or returning an Optional.empty() for a query for which the data is not found. We'll spend the remainder of this tutorial looking at examples of various ways to configure these mock beans.

The Spring MVC example application

To write Spring-based unit tests, we need an application to write them against. Fortunately, we can use the example application from my Spring Series tutorial "Mastering Spring framework 5, Part 1: Spring MVC." I used the example application from that tutorial as a base application. I modified it with a stronger REST API so that we'd have a few more things to test.

The example application is a Spring MVC web application with a REST controller, a service layer, and a repository that uses Spring Data JPA to persist "widgets" to and from an H2 in-memory database. Figure 1 is an overview.

Diagram of the Spring MVC example application. Steven Haines

Figure 1. A diagram of the Spring MVC example application

Unit testing a Spring service

Let's start by reviewing how to test a Spring service, because that is the easiest component in our MVC application to test. Examples in this section will allow us to explore JUnit 5's integration with Spring without introducing any new testing components or libraries, though we will do that later in the tutorial.

We'll begin by reviewing the WidgetService interface and the WidgetServiceImpl class, which are shown in Listing 1 and Listing 2, respectively.

Listing 1. The Spring service interface (WidgetService.java)


package com.geekcap.javaworld.spring5mvcexample.service;

import com.geekcap.javaworld.spring5mvcexample.model.Widget;

import java.util.List;
import java.util.Optional;

public interface WidgetService {
    Optional<Widget> findById(Long id);
    List<Widget> findAll();
    Widget save(Widget widget);
    void deleteById(Long id);
}

Listing 2. The Spring service implementation class (WidgetServiceImpl.java)


package com.geekcap.javaworld.spring5mvcexample.service;

import com.geekcap.javaworld.spring5mvcexample.model.Widget;
import com.geekcap.javaworld.spring5mvcexample.repository.WidgetRepository;
import com.google.common.collect.Lists;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Service
public class WidgetServiceImpl implements WidgetService {

    private WidgetRepository repository;

    public WidgetServiceImpl(WidgetRepository repository) {
        this.repository = repository;
    }

    @Override
    public Optional<Widget> findById(Long id) {
        return repository.findById(id);
    }

    @Override
    public List<Widget> findAll() {
        return Lists.newArrayList(repository.findAll());
    }

    @Override
    public Widget save(Widget widget) {
        // Increment the version number
        widget.setVersion(widget.getVersion()+1);

        // Save the widget to the repository
        return repository.save(widget);
    }

    @Override
    public void deleteById(Long id) {
        repository.deleteById(id);
    }
}

WidgetServiceImpl is a Spring service, annotated with the @Service annotation, that has a WidgetRepository wired into it through its constructor. The findById(), findAll(), and deleteById() methods are all passthrough methods to the underlying WidgetRepository. The only business logic you'll find is located in the save() method, which increments the version number of the Widget when it is saved.

The test class

In order to test this class, we need to create and configure a mock WidgetRepository, wire it into the WidgetServiceImpl instance, and then wire the WidgetServiceImpl into our test class. Fortunately, that's far easier than it sounds. Listing 3 shows the source code for the WidgetServiceTest class.

Listing 3. The Spring service test class (WidgetServiceTest.java)


package com.geekcap.javaworld.spring5mvcexample.service;

import com.geekcap.javaworld.spring5mvcexample.model.Widget;
import com.geekcap.javaworld.spring5mvcexample.repository.WidgetRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static org.mockito.Mockito.doReturn;
import static org.mockito.ArgumentMatchers.any;

@SpringBootTest
public class WidgetServiceTest {
    /**
     * Autowire in the service we want to test
     */
    @Autowired
    private WidgetService service;

    /**
     * Create a mock implementation of the WidgetRepository
     */
    @MockBean
    private WidgetRepository repository;

    @Test
    @DisplayName("Test findById Success")
    void testFindById() {
        // Setup our mock repository
        Widget widget = new Widget(1l, "Widget Name", "Description", 1);
        doReturn(Optional.of(widget)).when(repository).findById(1l);

        // Execute the service call
        Optional<Widget> returnedWidget = service.findById(1l);

        // Assert the response
        Assertions.assertTrue(returnedWidget.isPresent(), "Widget was not found");
        Assertions.assertSame(returnedWidget.get(), widget, "The widget returned was not the same as the mock");
    }

    @Test
    @DisplayName("Test findById Not Found")
    void testFindByIdNotFound() {
        // Setup our mock repository
        doReturn(Optional.empty()).when(repository).findById(1l);

        // Execute the service call
        Optional<Widget> returnedWidget = service.findById(1l);

        // Assert the response
        Assertions.assertFalse(returnedWidget.isPresent(), "Widget should not be found");
    }

    @Test
    @DisplayName("Test findAll")
    void testFindAll() {
        // Setup our mock repository
        Widget widget1 = new Widget(1l, "Widget Name", "Description", 1);
        Widget widget2 = new Widget(2l, "Widget 2 Name", "Description 2", 4);
        doReturn(Arrays.asList(widget1, widget2)).when(repository).findAll();

        // Execute the service call
        List<Widget> widgets = service.findAll();

        // Assert the response
        Assertions.assertEquals(2, widgets.size(), "findAll should return 2 widgets");
    }

    @Test
    @DisplayName("Test save widget")
    void testSave() {
        // Setup our mock repository
        Widget widget = new Widget(1l, "Widget Name", "Description", 1);
        doReturn(widget).when(repository).save(any());

        // Execute the service call
        Widget returnedWidget = service.save(widget);

        // Assert the response
        Assertions.assertNotNull(returnedWidget, "The saved widget should not be null");
        Assertions.assertEquals(2, returnedWidget.getVersion(), "The version should be incremented");
    }
}

The WidgetServiceTest class is annotated with the @SpringBootTest annotation, which scans the CLASSPATH for all Spring configuration classes and beans and sets up the Spring application context for the test class. Note that WidgetServiceTest also implicitly includes the @ExtendWith(SpringExtension.class) annotation, through the @SpringBootTest annotation, which integrates the test class with JUnit 5.

The test class also uses Spring's @Autowired annotation to autowire a WidgetService to test against, and it uses Mockito's @MockBean annotation to create a mock WidgetRepository. At this point, we have a mock WidgetRepository that we can configure, and a real WidgetService with the mock WidgetRepository wired into it.

Testing the Spring service

The first test method, testFindById(), executes WidgetService's findById() method, which should return an Optional<Widget> that contains a Widget. We begin by creating a Widget that we want the WidgetRepository to return. We then leverage the Mockito API to configure the WidgetRepository::findById method. The structure of our mock logic is as follows:


doReturn(VALUE_TO_RETURN).when(MOCK_CLASS_INSTANCE).MOCK_METHOD

In this case, we're saying: Return an Optional of our Widget when the repository's findById() method is called with an argument of 1 (as a long).

Next, we invoke the WidgetService's findById method with an argument of 1. We then validate that it is present and that the returned Widget is the one that we configured the mock WidgetRepository to return.

1 2 3 4 Page 1
Page 1 of 4