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. 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.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 customObjectMapper
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 customLocaleResolver
.
- If we use a custom
ObjectMapper
, we should write a factory method which returns a configuredObjectMapper
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 theFixedLocaleResolver
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() {} }
- 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 bepublic
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:
- Create new
MappingJackson2HttpMessageConverter
object. This object can read and write JSON by using Jackson. - Configure the
ObjectMapper
that's used by the createdMappingJackson2HttpMessageConverter
object. - 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:
- 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 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 custom error handler class (aka@ControllerAdvice
class) and a customHttpMessageConverter
which can read and write JSON by using Jackson (aka theMappingJackson2HttpMessageConverter
). - 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.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); } }
@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 haspublic
andstatic
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 theMockMvcBuilders
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 customHttpMessageConverter
which can read and write JSON by using Jackson (aka theMappingJackson2HttpMessageConverter
).
P.S. You can get the example application of this blog post from Github.