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. TheTodoItemCrudController
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 aLocale
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 theInternalResourceViewResolver
that'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
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:
- Create a new
SimpleMappingExceptionResolver
object. - Ensure that the system under test renders the 404 view when the
TodoItemNotFoundException
is thrown by the tested controller method. - Ensure that the system under test renders the error view when the tested controller method throws either
Exception
orRuntimeException
. - 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
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; } }
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:
- Create a new class.
- 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. - Ensure that we can inject the used
MockMvc
object into themockMvc
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; } }
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
requestBuilder
field contains theTodoItemRequestBuilder
object that's used by our test methods when they send HTTP requests to the system under test. - The
service
field contains aTodoItemCrudService
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:
- Create a new
TodoItemCrudService
mock and store the created mock in theservice
field. - Create a new
TodoItemCrudController
object (this is the tested controller) and store the created object in a local variable. - Create a new
MockMvc
object by using the standalone configuration and store the created object in a local variable. Remember to configure a customHandlerExceptionResolver
,LocaleResolver
, andViewResolver
. - Create a new
TodoItemRequestBuilder
object and store the created object in therequestBuilder
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
, andViewResolver
. - If we want to configure the system under test by using the standalone configuration, we have to invoke the
standaloneSetup()
method of theMockMvcBuilders
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.
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.