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
TodoItemCrudControllerclass contains the controller methods which render the todo items found from the database, create new todo items, and update existing todo items. - The
TodoItemCrudServiceclass provides CRUD operations for todo items. TheTodoItemCrudControllerclass 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
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 that our unit tests use theInternalResourceViewResolverthat's used by the Spring MVC Test framework 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, 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() {}
}
- 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 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:
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;
}
}
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
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:
import org.springframework.test.web.servlet.MockMvc;
class TodoItemRequestBuilder {
private final MockMvc mockMvc;
TodoItemRequestBuilder(MockMvc mockMvc) {
this.mockMvc = mockMvc;
}
}
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:
- 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:
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:
- Create a new
TodoItemCrudServicemock and store the created mock in theservicefield. - Create a new
TodoItemCrudControllerobject (this is the tested controller) and store the created object in a local variable. - Create a new
MockMvcobject by using the standalone configuration and store the created object in a local variable. Remember to configure a customHandlerExceptionResolver,LocaleResolver, andViewResolver. - Create a new
TodoItemRequestBuilderobject and store the created object in therequestBuilderfield.
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, andViewResolver. - 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.
Why not just use beforeAll instead of beforeEach ? And make service and requestBuilder static
I combined your comments - Petri
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.