Writing Unit Tests for "Normal" Spring MVC Controllers: Configuration

The previous part of my new Spring MVC Test tutorial taught us 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 render data and process form submissions.

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 normal Spring MVC controllers 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 render the todo items found from the database, create new todo items, and update existing 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.stereotype.Controller;

@Controller
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 three rules which help us to select the required components:

  • We should configure the used HandlerExceptionResolver if our web application has error views which are rendered when a controller method throws an exception.
  • We should specify the used LocaleResolver if a Locale object is injected into a method of the tested controller as a method parameter.
  • We should specify the used ViewResolver if we don’t want that our unit tests use the InternalResourceViewResolver that's used by the Spring MVC Test framework if no view resolver is configured.
We don't have to configure a custom ViewResolver because we will write assertions only for the name of the returned view. This means that we can use the InternalResourceViewResolver. However, I will configure a custom ViewResolver because I want to show you how you can do it.

We can create and configure these 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:

  • Our object mother class is public because we don't want to put our test classes in the same package as our object mother class.
  • 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 write a factory method that creates and configures the used HandlerExceptionResolver. In other words, we have add a public and static method to the WebTestConfig class. This method has no method parameters, and it returns a SimpleMappingExceptionResolver object.

After we have added this method to our object mother class, we have to implement it by following these steps:

  1. Create a new SimpleMappingExceptionResolver object.
  2. Ensure that the system under test renders the 404 view when the TodoItemNotFoundException is thrown by the tested controller method.
  3. Ensure that the system under test renders the error view when the tested controller method throws either Exception or RuntimeException.
  4. Ensure that the system under test returns the HTTP status code 404 when it renders the 404 view.
  5. Ensure that the system under test returns the HTTP status code 500 when it renders the error view.
  6. Return the created SimpleMappingExceptionResolver object.

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

import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import java.util.Properties;

public final class WebTestConfig {

    private WebTestConfig() {}

    public static SimpleMappingExceptionResolver exceptionResolver() {
        SimpleMappingExceptionResolver exceptionResolver = 
                new SimpleMappingExceptionResolver();

        Properties exceptionMappings = new Properties();

        exceptionMappings.put(
                "net.petrikainulainen.springmvctest.todo.TodoItemNotFoundException",
                "error/404"
        );
        exceptionMappings.put("java.lang.Exception", "error/error");
        exceptionMappings.put("java.lang.RuntimeException", "error/error");

        exceptionResolver.setExceptionMappings(exceptionMappings);

        Properties statusCodes = new Properties();

        statusCodes.put("error/404", "404");
        statusCodes.put("error/error", "500");

        exceptionResolver.setStatusCodes(statusCodes);

        return exceptionResolver;
    }
}

Third, we have to write a public and static factory method that creates and configures the used LocaleResolver. This method has no method parameters, and it returns a LocaleResolver object. When we implement this method, we have to return a new FixedLocaleResolver object that returns Locale.ENGLISH. It's a good idea to use a fixed locale because it ensures that the environment in which our tests are run cannot cause false positives (aka test failures).

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

import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.i18n.FixedLocaleResolver;

import java.util.Locale;
import java.util.Properties;

public final class WebTestConfig {

    private WebTestConfig() {}

    public static SimpleMappingExceptionResolver exceptionResolver() {
        SimpleMappingExceptionResolver exceptionResolver = 
                new SimpleMappingExceptionResolver();

        Properties exceptionMappings = new Properties();

        exceptionMappings.put(
                "net.petrikainulainen.springmvctest.todo.TodoItemNotFoundException",
                "error/404"
        );
        exceptionMappings.put("java.lang.Exception", "error/error");
        exceptionMappings.put("java.lang.RuntimeException", "error/error");

        exceptionResolver.setExceptionMappings(exceptionMappings);

        Properties statusCodes = new Properties();

        statusCodes.put("error/404", "404");
        statusCodes.put("error/error", "500");

        exceptionResolver.setStatusCodes(statusCodes);

        return exceptionResolver;
    }

    public static LocaleResolver fixedLocaleResolver() {
        return new FixedLocaleResolver(Locale.ENGLISH);
    }
}

Fourth, we have to write a public and static factory method that creates and configures the used ViewResolver. This method has no method parameters, and it returns a ViewResolver object. When we implement this method, we will create (and return) a new ViewResolver object that uses JSP views.

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

import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.i18n.FixedLocaleResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

import java.util.Locale;
import java.util.Properties;

public final class WebTestConfig {

    private WebTestConfig() {}

    public static SimpleMappingExceptionResolver exceptionResolver() {
        SimpleMappingExceptionResolver exceptionResolver = 
                new SimpleMappingExceptionResolver();

        Properties exceptionMappings = new Properties();

        exceptionMappings.put(
                "net.petrikainulainen.springmvctest.todo.TodoItemNotFoundException",
                "error/404"
        );
        exceptionMappings.put("java.lang.Exception", "error/error");
        exceptionMappings.put("java.lang.RuntimeException", "error/error");

        exceptionResolver.setExceptionMappings(exceptionMappings);

        Properties statusCodes = new Properties();

        statusCodes.put("error/404", "404");
        statusCodes.put("error/error", "500");

        exceptionResolver.setStatusCodes(statusCodes);

        return exceptionResolver;
    }

    public static LocaleResolver fixedLocaleResolver() {
        return new FixedLocaleResolver(Locale.ENGLISH);
    }

    public static ViewResolver jspViewResolver() {
        InternalResourceViewResolver viewResolver = 
                new InternalResourceViewResolver();

        viewResolver.setViewClass(JstlView.class);
        viewResolver.setPrefix("/WEB-INF/jsp/");
        viewResolver.setSuffix(".jsp");

        return viewResolver;
    }
}
If our application uses some other view technology, we should replace this method with a method that creates a ViewResolver object which supports the used view technology. Of course, we should do this only if we really need to configure a custom ViewResolver. Remember that the odds are that we don’t need it.

Additional Reading:

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 normal Spring MVC controllers.

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 (and test) 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 HandlerExceptionResolver, LocaleResolver, and ViewResolver.
  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.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
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);

        TodoItemController testedController = new TodoItemCrudController(service)
        MockMvc mockMvc = MockMvcBuilders
                .standaloneSetup(testedController)
                .setHandlerExceptionResolvers(exceptionResolver())
                .setLocaleResolver(fixedLocaleResolver())
                .setViewResolvers(jspViewResolver())
                .build();
        requestBuilder = new TodoItemRequestBuilder(mockMvc);
    }
}

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 an object mother class.
  • We can send HTTP requests to the system under test without writing duplicate code by using a request builder class.
  • The most common custom components which are included in the system under test are: HandlerExceptionResolver, LocaleResolver, and ViewResolver.
  • 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.

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

2 comments… add one
  • Schariss Aug 7, 2022 @ 1:22

    Why not just use beforeAll instead of beforeEach ? And make service and requestBuilder static

    I combined your comments - Petri

    • Petri Aug 10, 2022 @ 20:24

      The biggest reason is that test doubles aren't stateless. This means that creating a new "runtime environment" for each test method is the simplest way to ensure that each test method gets a "clean" environment.

      On the other hand, if you create your test doubles with Mockito, it's also possible to create the "runtime environment" only once. However, if you use this approach, you must remember to clean up the configuration of your test doubles before a test method is run and create a new configuration for the invoked test method. If you don't do this, you cannot guarantee that the test doubles used by the invoked test method behave as they should.

Leave a Reply