Writing Unit Tests for a Spring MVC REST API: Configuration

Earlier we learned that we should configure the system under test by using the standalone configuration when we are writing unit tests for Spring MVC controllers.

In this blog post, we will put theory into practice. This blog post describes how we can use the standalone configuration when we are writing unit tests for Spring MVC controllers which implement a REST API.

After we have finished this blog post, we:

  • Understand how we can create and configure the required components without adding duplicate code to our test suite.
  • Know how we can send HTTP requests to the system under test without adding duplicate code to our test suite.
  • Can configure the Spring MVC Test framework when we are writing unit tests for a Spring MVC REST API with JUnit 5.

Let's begin.

Introduction to the System Under Test

The system under test consists of two classes:

  • The TodoItemCrudController class contains the controller methods which implement a REST API that provides CRUD operations for todo items.
  • The TodoItemCrudService class provides CRUD operations for todo items. The TodoItemCrudController class invokes its methods when it processes HTTP requests.

The relevant part of the TodoItemCrudController class looks as follows:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/todo-item")
public class TodoItemCrudController {

    private final TodoItemCrudService service;

    @Autowired
    public TodoItemCrudController(TodoItemCrudService service) {
        this.service = service;
    }
}

Next, we will create the components which extend the minimum Spring MVC configuration that's created when we configure the system under test by using the standalone configuration.

Creating the Required Components

As we remember, we should minimize the number of custom components which we include in the system under test. However, it can be hard to identify the essential components if we don't have a lot of experience. That's why I wrote two rules which help us to select the required components:

  • We should create and configure a custom HttpMessageConverter if we want to use a custom ObjectMapper which transforms JSON into objects and vice versa.
  • If a Locale object is injected into a method of the tested controller as a method parameter, we must create and configure a custom LocaleResolver.
There are two things I want to point out:

  • If we use a custom ObjectMapper, we should write a factory method which returns a configured ObjectMapper object. This helps us to write tests which send JSON documents to the system under test. Also, this ensures that our test methods and the Spring MVC Test framework use the same configuration.
  • We don’t configure a custom LocaleResolver because our controller don’t use it. However, if you need one, you should use the FixedLocaleResolver class.

We can create and configure the required components by following these steps:

First, we have to create a public object mother class that contains the factory methods which create and configure the required components. After we have created our object mother class, we have to ensure that no one can instantiate it.

After we have created our object mother class, its source code looks as follows:

public final class WebTestConfig {
 
    private WebTestConfig() {}
}
There are two things I want to point out:

  • Because we don't want to put our test classes in the same package as our object mother class, our object mother class must be public. Also, this means that all factory methods added to our object mother class must be public as well.
  • We must use an object mother class because multiple test classes use the same components and we don't want to add duplicate code to our test suite.

Second, we have to add a public and static method called objectMapper() to our object mother class. This method creates and configures a new ObjectMapper object and returns the created object. Our tests will use this method when they create JSON documents which are send to system under test.

After we have written this method, the source code of our object mother class looks as follows:

import com.fasterxml.jackson.databind.ObjectMapper;

public final class WebTestConfig {
    
    private WebTestConfig() {}
    
    public static ObjectMapper objectMapper() {
        return new ObjectMapper();
    }
}

Third, we have to add a public and static method called objectMapperHttpMessageConverter() to our object mother class. Our test classes will use this method when they configure the system under test. After we have added this method to our object mother class, we have to implement it by following these steps:

  1. Create new MappingJackson2HttpMessageConverter object. This object can read and write JSON by using Jackson.
  2. Configure the ObjectMapper that's used by the created MappingJackson2HttpMessageConverter object.
  3. Return the created object.

After we have implemented this method, the source code of our object mother class looks as follows:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

public final class WebTestConfig {

    private WebTestConfig() {}

    public static MappingJackson2HttpMessageConverter objectMapperHttpMessageConverter() {
        MappingJackson2HttpMessageConverter converter = 
                new MappingJackson2HttpMessageConverter();
        converter.setObjectMapper(objectMapper());
        return converter;
    }

    public static ObjectMapper objectMapper() {
        return new ObjectMapper();
    }
}

We can now create the required components by using an object mother class. Let’s move on and find out how we can create a request builder class which sends HTTP requests to the system under test.

Creating the Request Builder Class

When we write unit tests for a real life web application or a REST API, we notice that every test method creates a new HTTP request and sends it to the system under test. This is a bad situation because duplicate code makes our tests hard to write and maintain.

We can solve this problem by using request builder classes. A request builder class is a class that fulfills these conditions:

  • It contains methods which create and send HTTP requests to the system under test by using a MockMvc object.
  • Every method must return a ResultActions object which allows us to write assertions for the returned HTTP response.

We can write our request builder class by following these steps:

  1. Create a new class.
  2. Add a private MockMvc field to the created class. Our request builder class will use this field when it creates and sends HTTP requests to the system under test.
  3. Ensure that we can inject the used MockMvc object into the mockMvc field by using constructor injection.

After we have created our request builder class, its source code looks as follows:

import org.springframework.test.web.servlet.MockMvc;

class TodoItemRequestBuilder {

    private final MockMvc mockMvc;

    TodoItemRequestBuilder(MockMvc mockMvc) {
        this.mockMvc = mockMvc;
    }
}
This request builder class is useless because we didn’t write the methods which create and send HTTP requests to the system under test. We will talk more about request builder classes when we learn to write unit tests for a Spring MVC REST API.

Additional Reading:

Next, we will learn to configure the system under test.

Configuring the System Under Test

We can create a new test class and configure the system under test by following these steps:

First, we have to create a new test class and add the required fields to our test class. Our test class has two private fields:

  1. The requestBuilder field contains the TodoItemRequestBuilder object that's used by our test methods when they send HTTP requests to the system under test.
  2. The service field contains a TodoItemCrudService mock. Our setup methods will use this field when they stub methods with Mockito. Also, our test methods will use this field when they verify the interactions that happened or didn't happen between the system under test and our mock.

After we have created our test class, its source code looks as follows:

class TodoItemCrudControllerTest {

    private TodoItemRequestBuilder requestBuilder;
    private TodoItemCrudService service;
}

Second, we have write a new setup method that's run before a test method is run, and implement this method by following these steps:

  1. Create a new TodoItemCrudService mock and store the created mock in the service field.
  2. Create a new TodoItemCrudController object (this is the tested controller) and store the created object in a local variable.
  3. Create a new MockMvc object by using the standalone configuration and store the created object in a local variable. Remember to configure a custom error handler class (aka @ControllerAdvice class) and a custom HttpMessageConverter which can read and write JSON by using Jackson (aka the MappingJackson2HttpMessageConverter).
  4. Create a new TodoItemRequestBuilder object and store the created object in the requestBuilder field.

After we have written our setup method, the source code of our test class looks as follows:

import org.junit.jupiter.api.BeforeEach;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*;
import static org.mockito.Mockito.mock;

class TodoItemCrudControllerTest {

    private TodoItemRequestBuilder requestBuilder;
    private TodoItemCrudService service;

    @BeforeEach
    void configureSystemUnderTest() {
        service = mock(TodoItemCrudService.class);

        TodoItemCrudController testedController = new TodoItemCrudController(service);
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(testedController)
                .setControllerAdvice(new TodoItemErrorHandler())
                .setMessageConverters(objectMapperHttpMessageConverter())
                .build();
        requestBuilder = new TodoItemRequestBuilder(mockMvc);
    }
}
If the system under test throws custom exceptions and we want to ensure that these exceptions are handled properly, we must configure the used @ControllerAdvice class (aka error handler class).

Additional Reading:

We can now configure the system under test by using the standalone configuration. Let’s summarize what we learned from this blog post.

Summary

This blog post has taught us that:

  • We can create the required custom components without writing duplicate code by using a public object mother class that has public and static factory methods.
  • We can send HTTP requests to the system under test without writing duplicate code by using a request builder class.
  • If we want to configure the system under test by using the standalone configuration, we have to invoke the standaloneSetup() method of the MockMvcBuilders class.
  • We can include custom components in the system under test by using the methods of the StandaloneMockMvcBuilder class.
  • The most common custom components which are included in the system under test are: a custom @ControllerAdvice class and and a custom HttpMessageConverter which can read and write JSON by using Jackson (aka the MappingJackson2HttpMessageConverter).

P.S. You can get the example application of this blog post from Github.

0 comments… add one

Leave a Reply