This blog post describes how we can configure the system under test by using 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:
- Know how we can get the required dependencies.
- 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 Spring MockMvc when we are writing unit tests for view-rendering Spring MVC controllers with JUnit Jupiter.
Let’s begin.
This blog post assumes that:
Introduction to System Under Test
The TodoItemCrudController class contains the controller methods which render the todo items queried from the database, create new todo items, and update existing todo items. It has two dependencies:
- The
org.springframework.context.MessageSourceprovides the localized feedback messages which are displayed on the rendered view. - The
TodoItemCrudServiceclass provides CRUD operations for todo items. TheTodoItemCrudControllerclass invokes its methods when it processes HTTP requests.
Both of these dependencies are injected into the controller class by using constructor injection. The relevant part of the TodoItemCrudController class looks as follows:
1import org.springframework.beans.factory.annotation.Autowired;
2import org.springframework.context.MessageSource;
3import org.springframework.stereotype.Controller;
4import org.springframework.web.bind.annotation.RequestMapping;
5
6@Controller
7@RequestMapping("/todo-item")
8public class TodoItemCrudController {
9
10 private final MessageSource messageSource;
11 private final TodoItemCrudService service;
12
13 @Autowired
14 public TodoItemCrudController(MessageSource messageSource,
15 TodoItemCrudService service) {
16 this.messageSource = messageSource;
17 this.service = service;
18 }
19}
Next, we will learn to get the required dependencies.
Getting the Required Dependencies
The first thing that we have to do is to ensure that our build script declares the required testing dependencies. Our test suite uses the following testing tools:
- JUnit Jupiter. A new programming and extension model that is used to write both tests and extensions with JUnit.
- Spring MockMvc. A framework provided by Spring Test. Spring MockMvc allows us to send simulated HTTP requests to our controller without running a real servlet container.
- Hamcrest. An assertion library that’s used to write assertions for the HTTP response returned by the system under test.
- Mockito. A mocking framework that’s used to replace the dependencies of the system under test with test doubles.
- AssertJ. An assertion library that allows us to write readable assertions with its fluent API.
When we declare dependencies in our build script, we should use Spring Boot dependency management. The benefit of this approach is that we don’t have to manage dependency versions and worry about the compatibility of individual dependencies. Spring Boot dependency management takes care of these things for us. If we use the Spring Boot dependency management, we can get the required testing dependencies by declaring the spring-boot-starter-test dependency in our build script.
This tutorial doesn’t explain how we can use the Spring Boot dependency management. The Spring Boot user guide provides more information about this matter:
- Spring Boot User Guide: Using the Maven Plugin
- Spring Boot User Guide: Managing Dependencies With the Gradle Plugin
Additional Reading:
Let’s move on and find out how we can create the required components.
Creating the Required Components
As we remember, we should minimize the number of custom components which we include in the system under test. However, sometimes it’s hard to identify the essential components. That’s why I introduce three rules which help us to select the required components:
- We should configure the used
HandlerExceptionResolverif our web application has error views which are rendered when a controller method throws an exception. - We should specify the used
LocaleResolverif aLocaleobject is injected into a method of the tested controller as a method parameter. - We should specify the used
ViewResolverif we don’t want our unit tests to use theInternalResourceViewResolverthat’s used by Spring MockMvc if no view resolver is configured.
ViewResolver because we will write assertions only for the name of the returned view. This means that we can use the InternalResourceViewResolver. However, this blog post describes how we can configure a custom ViewResolver because it’s useful to know how we 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:
1public final class WebTestConfig {
2
3 private WebTestConfig() {}
4}
There are two things I want to point out:
- Our object mother class is
publicbecause 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 to 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:
- Create a new
SimpleMappingExceptionResolverobject. - Ensure that the system under test renders the 404 view when the
TodoItemNotFoundExceptionis thrown by the tested controller method. - Ensure that the system under test renders the error view when the tested controller method throws either
ExceptionorRuntimeException. - Ensure that the system under test returns the HTTP status code 404 when it renders the 404 view.
- Ensure that the system under test returns the HTTP status code 500 when it renders the error view.
- Return the created
SimpleMappingExceptionResolverobject.
After we have written our factory method, the source code of our object mother class looks as follows:
1import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
2
3import java.util.Properties;
4
5public final class WebTestConfig {
6
7 private WebTestConfig() {}
8
9 public static SimpleMappingExceptionResolver exceptionResolver() {
10 SimpleMappingExceptionResolver exceptionResolver =
11 new SimpleMappingExceptionResolver();
12
13 Properties exceptionMappings = new Properties();
14
15 exceptionMappings.put(
16 "net.petrikainulainen.springmvctest.todo.TodoItemNotFoundException",
17 "error/404"
18 );
19 exceptionMappings.put("java.lang.Exception", "error/error");
20 exceptionMappings.put("java.lang.RuntimeException", "error/error");
21
22 exceptionResolver.setExceptionMappings(exceptionMappings);
23
24 Properties statusCodes = new Properties();
25
26 statusCodes.put("error/404", "404");
27 statusCodes.put("error/error", "500");
28
29 exceptionResolver.setStatusCodes(statusCodes);
30
31 return exceptionResolver;
32 }
33}
Additional Reading:
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:
1import org.springframework.web.servlet.LocaleResolver;
2import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
3import org.springframework.web.servlet.i18n.FixedLocaleResolver;
4
5import java.util.Locale;
6import java.util.Properties;
7
8public final class WebTestConfig {
9
10 private WebTestConfig() {}
11
12 public static SimpleMappingExceptionResolver exceptionResolver() {
13 SimpleMappingExceptionResolver exceptionResolver =
14 new SimpleMappingExceptionResolver();
15
16 Properties exceptionMappings = new Properties();
17
18 exceptionMappings.put(
19 "net.petrikainulainen.springmvctest.todo.TodoItemNotFoundException",
20 "error/404"
21 );
22 exceptionMappings.put("java.lang.Exception", "error/error");
23 exceptionMappings.put("java.lang.RuntimeException", "error/error");
24
25 exceptionResolver.setExceptionMappings(exceptionMappings);
26
27 Properties statusCodes = new Properties();
28
29 statusCodes.put("error/404", "404");
30 statusCodes.put("error/error", "500");
31
32 exceptionResolver.setStatusCodes(statusCodes);
33
34 return exceptionResolver;
35 }
36
37 public static LocaleResolver fixedLocaleResolver() {
38 return new FixedLocaleResolver(Locale.ENGLISH);
39 }
40}
Additional Reading:
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 InternalResourceViewResolver object that uses JSP views.
After we have written our factory method, the source code of our object mother class looks as follows:
1import org.springframework.web.servlet.LocaleResolver;
2import org.springframework.web.servlet.ViewResolver;
3import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
4import org.springframework.web.servlet.i18n.FixedLocaleResolver;
5import org.springframework.web.servlet.view.InternalResourceViewResolver;
6import org.springframework.web.servlet.view.JstlView;
7
8import java.util.Locale;
9import java.util.Properties;
10
11public final class WebTestConfig {
12
13 private WebTestConfig() {}
14
15 public static SimpleMappingExceptionResolver exceptionResolver() {
16 SimpleMappingExceptionResolver exceptionResolver =
17 new SimpleMappingExceptionResolver();
18
19 Properties exceptionMappings = new Properties();
20
21 exceptionMappings.put(
22 "net.petrikainulainen.springmvctest.todo.TodoItemNotFoundException",
23 "error/404"
24 );
25 exceptionMappings.put("java.lang.Exception", "error/error");
26 exceptionMappings.put("java.lang.RuntimeException", "error/error");
27
28 exceptionResolver.setExceptionMappings(exceptionMappings);
29
30 Properties statusCodes = new Properties();
31
32 statusCodes.put("error/404", "404");
33 statusCodes.put("error/error", "500");
34
35 exceptionResolver.setStatusCodes(statusCodes);
36
37 return exceptionResolver;
38 }
39
40 public static LocaleResolver fixedLocaleResolver() {
41 return new FixedLocaleResolver(Locale.ENGLISH);
42 }
43
44 public static ViewResolver jspViewResolver() {
45 InternalResourceViewResolver viewResolver =
46 new InternalResourceViewResolver();
47
48 viewResolver.setViewClass(JstlView.class);
49 viewResolver.setPrefix("/WEB-INF/jsp/");
50 viewResolver.setSuffix(".jsp");
51
52 return viewResolver;
53 }
54}
If our application uses some other view technology, we should replace this method with a method that creates a ViewResolver object that supports the used view technology. Of course, we should do this only if we really need to configure a custom ViewResolver.
Additional Reading:
We can now create the required components by using an object mother class. Next, we will 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. If we create and send these HTTP requests by using the MockMvc API in our test methods, we add a lot of duplicate code to our test suite. This is a problem 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
MockMvcobject. - Every method must return a
ResultActionsobject which allows us to write assertions for the returned HTTP response.
We can write our request builder class by following these steps:
- Create a new class.
- Add a
private MockMvcfield to the created class. Our request builder class will use this field when it creates and sends HTTP requests to the system under test. - Ensure that we can inject the used
MockMvcobject into themockMvcfield by using constructor injection.
After we have created our request builder class, its source code looks as follows:
1import org.springframework.test.web.servlet.MockMvc;
2
3class TodoItemRequestBuilder {
4
5 private final MockMvc mockMvc;
6
7 TodoItemRequestBuilder(MockMvc mockMvc) {
8 this.mockMvc = mockMvc;
9 }
10}
There are two things I want to point out:
- Our request builder class is package private because we should put it to the same package as the test class that uses it.
- 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 view-rendering Spring MVC controllers.
Additional Reading:
Let’s move on and find out how we can configure the system under test.
Configuring the System Under Test
If our test suite has a lot of tests for Spring MVC controllers, there is a risk that we add a lot of duplicate configuration code to our test suite. That’s why we should create a new MockMvc object by using an object mother class. We can create this class by following these steps:
First, we have to create a public object mother class. 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:
1public final class MockMvcConfig {
2
3 private MockMvcConfig() {}
4}
Second, we have to add a new factory method to the MockMvcConfig class. This method takes the tested controller object as a method parameter, creates and configures a new MockMvc object, and returns the created object.
After we have added this method to our object mother class, its source code looks as follows:
1import org.springframework.test.web.servlet.MockMvc;
2
3public final class MockMvcConfig {
4
5 private MockMvcConfig() {}
6
7 public static MockMvc mockMvcForController(Object controller) {
8
9 }
10}
Third, we have to write the code that creates a new MockMvc object, configures the required components, and returns the created MockMvc object.
After we have implemented our factory method, the source code of the MockMvcConfig class looks as follows:
1import org.springframework.test.web.servlet.MockMvc;
2import org.springframework.test.web.servlet.setup.MockMvcBuilders;
3
4import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*;
5
6public final class MockMvcConfig {
7
8 private MockMvcConfig() {}
9
10 public static MockMvc mockMvcForController(Object controller) {
11 return MockMvcBuilders.standaloneSetup(controller)
12 .setHandlerExceptionResolvers(exceptionResolver())
13 .setLocaleResolver(fixedLocaleResolver())
14 .setViewResolvers(jspViewResolver())
15 .build();
16 }
17}
We can now 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 three private fields:
- The
messageSourcefield contains aStaticMessageSourceobject. Our setup methods will use this object when they configure the feedback messages that should be shown on the rendered view. - The
requestBuilderfield contains theTodoItemRequestBuilderobject that’s used by our test methods when they send HTTP requests to the system under test. - The
servicefield contains aTodoItemCrudServicemock. 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:
1import org.springframework.context.support.StaticMessageSource;
2
3class TodoItemCrudControllerTest {
4
5 private final StaticMessageSource messageSource = new StaticMessageSource();
6 private TodoItemRequestBuilder requestBuilder;
7 private TodoItemCrudService service;
8}
Second, we have to write a new setup method that’s run before a test method is run, and implement this method by following these steps:
- Create a new
TodoItemCrudServicemock and store the created mock in theservicefield. - Create a new
MockMvcobject by invoking thestatic mockMvcForController()method and passing the tested controller object as a method parameter. - Create a new
TodoItemRequestBuilderobject and store the created object in therequestBuilderfield.
After we have implemented our setup method, the source code of our test class looks as follows:
1import net.petrikainulainen.springmvctest.junit5.todo.TodoItemCrudService;
2import org.junit.jupiter.api.BeforeEach;
3import org.springframework.context.support.StaticMessageSource;
4
5import static net.petrikainulainen.springmvctest.junit5.web.MockMvcConfig.mockMvcForController;
6import static org.mockito.Mockito.mock;
7
8class TodoItemCrudControllerTest {
9
10 private final StaticMessageSource messageSource = new StaticMessageSource();
11 private TodoItemRequestBuilder requestBuilder;
12 private TodoItemCrudService service;
13
14 @BeforeEach
15 void configureSystemUnderTest() {
16 service = mock(TodoItemCrudService.class);
17
18 var mockMvc = mockMvcForController(
19 new TodoItemCrudController(messageSource, service)
20 );
21 requestBuilder = new TodoItemRequestBuilder(mockMvc);
22 }
23}
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 should use Spring Boot dependency management and get the required dependencies by declaring the
spring-boot-starter-testdependency in our build script. - We can create the required 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, andViewResolver. - We can create
MockMvcobjects without adding duplicate configuration code to our test suite by using an object mother class. - If we want to configure the system under test by using the standalone configuration, we have to invoke the
standaloneSetup()method of theMockMvcBuildersclass. - We can include custom components in the system under test by using the methods of the
StandaloneMockMvcBuilderclass.
P.S. You can get the example application of this blog post from GitHub.