How to test the Web Layer of your Spring Boot Application with @WebMvcTest


The Web Layer is an interface that enables systems to access the business logic of your application. It is responsible to translate the web-based requests into a format that is used in the core of your system. Also, it transforms the internally used formats into a response that can be sent over the web.
To lower the risk of changing the behavior of your Web Layer by accident, you can use @WebMvcTests.

What exactly is a @WebMvcTest?

@WebMvcTest is one of Spring Boot’s test slices. Integration tests with this annotation will only load a specific subset of beans into the ApplicationContext. Those beans are all part of the context of the Spring MVC Module. Or as the documentation says:

Using this annotation will disable full auto-configuration and instead apply only configuration relevant to MVC tests (i.e. @Controller, @ControllerAdvice, @JsonComponent, Converter/GenericConverter, Filter, WebMvcConfigurer and HandlerMethodArgumentResolver beans but not @Component, @Service or @Repository beans)

Reducing the scope of the beans can lead to a shorter startup time for your integration tests.

To use @WebMvcTest, you need to include the dependency org.springframework.boot:spring-boot-starter-test in your project.

pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

The Example

Before we start coding the @WebMvcTest, I will give you a quick overview of the classes that will be involved in our test. The BookController is a @RestController and thus part of the Web Layer that we want to test.

BookController.java
@RestController
public class BookController {

    private final BookRepository bookRepository;

    public BookController(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    @GetMapping(path = "/", produces = MediaType.APPLICATION_JSON_VALUE)
    public List<Book> findAll() {
        return bookRepository.findAll();
    }

    @GetMapping(path = "/{isbn}", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Book> findByIsbn(@PathVariable String isbn) {
        var book = bookRepository.findByIsbn(isbn)
                .orElseThrow(() -> BookNotFoundException.unknownIsbn(isbn));

        return ResponseEntity.ok(book);
    }

    @ResponseStatus(value = HttpStatus.NOT_FOUND)
    private static class BookNotFoundException extends RuntimeException {

        private BookNotFoundException(String message) {
            super(message);
        }

        public static BookNotFoundException unknownIsbn(String isbn){
            return new BookNotFoundException("Can not find book with ISBN '%s'".formatted(isbn));
        }
    }
}

It exposes two endpoints that can be used to retrieve either all books (findAll) or one specific book identified by its ISBN (findByIsbn). If the ISBN belongs to an unknown book, the controller will throw a BookNotFoundException which will be converted into a 404 (NOT FOUND) response. The data of the response will always be in JSON format.

The controller depends on a BookRepository which loads data from a data store. To keep things simple, we won’t use a database but a simple List<Book>.

InMemoryBookRepository.java
@Repository
public class InMemoryBookRepository implements BookRepository{

    private List<Book> books = List.of(new Book("Spring Boot 2", "Michael Simons", "978-3-86490-525-4")
            , new Book("Langlebige Software-Architekturen", "Carola Lilienthal", "978-3-86490-729-6"));

    @Override
    public List<Book> findAll() {
        return Collections.unmodifiableList(books);
    }

    @Override
    public Optional<Book> findByIsbn(String isbn) {
        return books.stream().filter(book -> book.getIsbn().equals(isbn)).findFirst();
    }
}

Test Setup

The following listing shows the basic structure of the test class.

BookControllerTest.java
@WebMvcTest({BookController.class})
class BookControllerTest {

    @MockBean
    private BookRepository bookRepository;

    @Autowired
    private MockMvc mockMvc;

    // Tests
}

The first step of the setup is the @WebMvcTest annotation on the test class itself. As mentioned above, it ensures that only beans relevant to the Web layer are loaded into the ApplicationContext. You can reduce this scope even more by passing specific controllers as value into the annotation. In this example, the BookController is the only controller that will be loaded.

The @MockBean annotation adds a mock into the ApplicationContext. This is required to provide a BookRepository bean that can be injected into the BookController. Otherwise, the test could not start and would fail. The real implementation – the InMemoryBookRepository – is not loaded because of its @Repository annotation.

The last part of the setup code is the MockMvc. It is part of Spring’s testing tools and helps you to send requests and make assertions on the response.

Testing for the correct status code

With the basic setup done, we can now start implementing the first test. One part of the behavior of our controller is to return the correct HTTP status code. If the request succeeded, the server responds with a 200 (OK) status code. If the requested resource could not be found, it responds with a 404 NOT FOUND.

BookControllerTest.java
@Test
void returnsListOfBooks() throws Exception {
    mockMvc.perform(get("/"))
            .andExpect(status().isOk());
}

We use MockMvc to perform a GET-Request against “/” and expect to receive a response with the status code 200 (OK). The fluent API of MockMvc allows us to describe our test in a way we would do with a natural language. With the help of the autocompletion of your IDE and a bit of practice, it becomes a breeze to test your Web Layer.

Testing for the status code 404 (NOT FOUND) needs a bit of preparation. To make sure that it’s impossible to find an existing book by accident we use our mocked BookRepository.

BookControllerTest.java
@Test
void returns404_WhenThereIsNoBookWithTheGivenIsbn() throws Exception {
    doReturn(Optional.empty()).when(bookRepository).findByIsbn(anyString());

    mockMvc.perform(get("/123-4-56789-123-3"))
            .andExpect(status().isNotFound());
}

With the first line of the test, we mock the behavior of the BookRepository. Every time findByIsbn is called with any string, the mock will return an empty Optional. The other part of the test is nearly identical to the previous one. We use MockMvc to perform a GET request and test for the correct status code.

Testing for the correct content

In the previous tests, we made assertions about the status code of the responses. In this example, we will extend the first test case to test the arguably more important part – the content itself. Like before we use MockMvc for our assertions.

BookControllerTest.java
@Test
void returnsListOfBooks() throws Exception {
    var books = List.of(new Book("Spring Boot 2", "Michael Simons", "978-3-86490-525-4")
    , new Book("Langlebige Software-Architekturen", "Carola Lilienthal", "978-3-86490-729-6"));

    doReturn(books).when(bookRepository).findAll();

    mockMvc.perform(get("/"))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(jsonPath("$", hasSize(2)))
            .andExpect(jsonPath("$.[0].title", is(books.get(0).getTitle())))
            .andExpect(jsonPath("$.[0].author", is(books.get(0).getAuthor())))
            .andExpect(jsonPath("$.[0].isbn", is(books.get(0).getIsbn())));
}

The first lines are used to mock the behavior of the BookRepository. Every time its findAll method is called, it will return a hardcoded list of books.

The new assertions can be found in lines 10-14. We can use MockMvc::andExpect in combination with the jsonPath method to test the structure and the content of our response. The first parameter of jsonPath – the expression – is a JSONPath. It is used to address a specific part of a JSON structure. If you want to learn more about the syntax of JSONPath, I recommend having a look at this article.

Conclusion

Spring Boot offers a great toolset when it comes to testing your application. You can use MockMvc to call the endpoints of your controllers and make assertions about the response. To support you with the assertions, Spring Boot provides a collection of predefined ResultMatchers that can be accessed via org.springframework.test.web.servlet.result.MockMvcResultMatchers.
By using @WebMvcTest only beans relevant to the Web Layer will be loaded into the ApplicationContext. This can reduce the execution time of your tests, which will lead to faster feedback in the development process.

Resources