MockMvc – Spring MVC testing framework introduction: Testing Spring endpoints
Introduction
In this post we’ll see how to use MockMvc to test Spring endpoints. This is the first post of a series, this post will highlight the advantages of using Spring MVC test framework compared to other ways of testing controller classes and what are the different ways to setup your tests.
You can find the source code for this post at GitHub. The project contains a regular MVC controller that forwards requests to a static resource, and two rest controllers that return a list of languages and coffees.
MockMvc and the test pyramid
In his book, Succeeding with Agile: Software Development Using Scrum, Mike Cohn describes the test automation pyramid as a test automation strategy in terms of quantity and effort that must be spent in the different types of tests. The test pyramid is also used by other authors to describe a test portfolio in terms of cost and speed. The base of the pyramid are unit tests, meaning that unit tests are the foundation of the testing strategy and that there should be many more unit tests than high level end-to-end tests.
As you know, unit testing focuses on testing the smallest component of a software. In object oriented programming this is usually a single class or interface, or a method within this class. The test is performed in isolation of other units, in order to achieve this dependencies are usually mocked. One of the main traits of unit tests is that their execution is really fast.
Similarly, integration tests focus on testing a combination or group of the aforementioned componentes. Technically speaking, tests using MockMvc
are in the boundaries between unit and integration tests. They aren’t unit tests because endpoints are tested in integration with a Mocked MVC container with mocked inputs and dependencies. But we might also don’t consider these tests as integration tests, if done right, with MockMvc
we are only testing a single component with a mocked platform, but not a combination of components.
Technicalities aside, when preparing a test portfolio, tests using Spring MVC test framework should be treated as unit tests and be part of the foundation of the testing strategy.
Language Controller
For the purpose of this tutorial we are going to test a simple controller that returns a filtered list of programming languages.
1@Controller
2@RequestMapping(value = "/api", produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
3public class LanguageApiController {
4
5 private final LanguageService languageService;
6
7 @Autowired
8 public LanguageApiController(
9 LanguageService languageService) {
10 this.languageService = languageService;
11 }
12
13 @RequestMapping(value = "/languages", method = GET)
14 public ResponseEntity<List<Language>> getLanguages(@RequestParam(value = "contains", required = false) String contains) {
15 return ResponseEntity.ok(languageService.getLanguages(contains));
16 }
17
18 @RequestMapping(value = "/languages/{name}", method = GET)
19 public ResponseEntity<Language> getLanguage(@PathVariable("name") String name) {
20 return ResponseEntity.ok(languageService.getLanguage(name).orElseThrow(() -> new SpringMockMvcException(
21 HttpStatus.NOT_FOUND, "Language was not found")));
22 }
23
24 @ExceptionHandler(SpringMockMvcException.class)
25 public ResponseEntity<String> onSpringMockMvcException(HttpServletRequest request, SpringMockMvcException ex) {
26 return ResponseEntity.status(ex.getHttpStatus()).body(String.format("%s - %s",
27 ex.getHttpStatus().value(), ex.getMessage()));
28 }
29
30}
The controller has a global @RequestMapping
annotation specifying the output mime-type for the endpoints declared in the class as application/json
. The class exposes two endpoints that rely on LanguageService
to perform business operations.
The first endpoint (/api/languages
) will retrieve a list of languages that can be optionally filtered with a parameter (contains
).
The second endpoint (/api/language/{name}
) will return a single language filtered by it’s unique name.
Finally there’s a an ExceptionHandler
that will transform any controlled SpringMockMvcException
into a ResponseEntity
.
Spring MVC test framework advantages
As already stated, tests running with MockMvc
lie somewhere in the boundaries between integration and unit tests. Following is a strict unit test that will test the LanguageApiController
.
1@Test
2 public void getLanguages_null_shouldReturnListOfStrings() throws Exception {
3 // Given
4 final String mockedEsoteric = "Arnoldc";
5 final List<String> mockedLanguages = Stream.concat(
6 LanguageRepository.LANGUAGES.stream(),
7 Stream.of(mockedEsoteric)).collect(Collectors.toList());
8 doReturn(mockedLanguages).when(languageService).getLanguages(null);
9
10 // When
11 final ResponseEntity<List<String>> result = languageApiController.getLanguages(null);
12
13 // Then
14 assertThat(result.getBody(), hasSize(mockedLanguages.size()));
15 assertThat(result.getBody(), hasItem(mockedEsoteric));
16 }
17}
This test will validate that the method returns the expected list whenever a null argument is passed. It will add 100% coverage for the exposed controller method, even though many aspects of the implementation won’t be tested, more specifically:
- Content negotiation headers: This specific controller is configured to produce only
application/json
content. - Response codes: With
MockMvc
we’re able to check if the response code matches the expected one, even if it’s produced outside the controller method or post processed by an annotation/aspect. - JSON serialization/deserialization:
MockMvc
will help us validate that JSON is deserialized as expected when performing a request with content, and correctly converted to the response body. - Availability of response headers
- Many other scenarios and configurations that happen outside the method specific implementation but are part of the endpoint implementation (Validation, Security, Exception Handling…)
WebMvcTest annotation
The easiest way to run a test with MockMvc
is to use the @WebMvcTest
Spring boot annotation. This annotation will configure the SpringRunner
JUnit test runner to deploy a partial webapplication by auto configuring only the required components for an MVC application.
In my opinion, this is not the best approach to test your application as for each test suite run a mocked application will be deployed. This may be an overkill if you only want to test units of a controller and not the integration of the controller with the rest of components (test pyramid).
The following test will use @WebMvcTest
to autoconfigure the test environment and perform the test validations using an injected MockMvc
instance:
1@RunWith(SpringRunner.class)
2@WebMvcTest(controllers = LanguageApiController.class)
3public class LanguageApiControllerWebMvcTest {
4
5 @Autowired
6 private MockMvc mockMvc;
7
8 @Test
9 public void getLanguages_null_shouldReturnOk() throws Exception {
10 // Given
11 // Real application context
12
13 // When
14 final ResultActions result = mockMvc.perform(
15 get("/api/languages")
16 .accept(MimeTypeUtils.APPLICATION_JSON_VALUE));
17
18 // Then
19 final int expectedSize = LANGUAGES.size();
20 final String[] expectedLanguageNames = LANGUAGES.stream().map(Language::getName)
21 .collect(Collectors.toList()).toArray(new String[LANGUAGES.size()]);
22 result.andExpect(status().isOk());
23 result.andExpect(jsonPath("$.length()").value(expectedSize));
24 result.andExpect(jsonPath("$[*].name", containsInAnyOrder(expectedLanguageNames)));
25 }
26}
Again, this is not the best approach if we want to have a test portfolio following the test pyramid strategy. An alternative methodology is to instantiate MockMvc
programmatically is described in the following sections.
Web Application Context integration setup
There are several ways in which MockMvc
can be setup programmatically, these settings will set the degree of integration your tests will perform. The highest level of integration with MockMvc
will be achieved using .webAppContextSetup(...)
. In my opinion this is not the best approach to test your endpoints as many of the advantages of unit testing are lost (tests are slower, increased complexity to mock objects…).
The following test verifies the application behavior using this technique:
1@RunWith(SpringJUnit4ClassRunner.class)
2@WebAppConfiguration
3@ContextConfiguration(classes = {
4 SpringMockMvcConfiguration.class
5})
6public class LanguageApiControllerApplicationIntegrationTest {
7
8 @Autowired
9 private WebApplicationContext webApplicationContext;
10
11 private MockMvc mockMvc;
12
13 @Before
14 public void setUp() {
15 mockMvc = MockMvcBuilders
16 .webAppContextSetup(webApplicationContext)
17 .build();
18 }
19/*...*/
20 @Test
21 public void getLanguages_null_shouldReturnOk() throws Exception {
22 // Given
23 // Real application context
24
25 // When
26 final ResultActions result = mockMvc.perform(
27 get("/api/languages")
28 .accept(MimeTypeUtils.APPLICATION_JSON_VALUE));
29
30 // Then
31 final int expectedSize = LANGUAGES.size();
32 final String[] expectedLanguageNames = LANGUAGES.stream().map(Language::getName)
33 .collect(Collectors.toList()).toArray(new String[LANGUAGES.size()]);
34 result.andExpect(status().isOk());
35 result.andExpect(jsonPath("$.length()").value(expectedSize));
36 result.andExpect(jsonPath("$[*].name", containsInAnyOrder(expectedLanguageNames)));
37 }
38
39}
There are two important sections in this code. In the class annotations section we find @WebAppConfiguration
which indicates that a WebApplicationContext
should be loaded for the test using a default path to the web application root. Additionally we define a @ContextConfiguration
to indicate which classes/beans should be loaded for the test. In this case we are reusing the same configuration bean used for the real application, so we’ll be able to completely test the application’s component integration.
The next important section in the code is the test setup. In this case we are building a MockMvc instance using the fully initialized WebApplicationContext
. Spring will autowire this context as specified by the @WebAppConfiguration
annotation.
The test validates that the endpoint is exposed in the expected path and that it sends the real repository’s languages when receiving a request with the appropriate headers. So on top of validating the controller implementation, we are validating that the aspect configuration works as expected.
As already stated, this test shares the real application WebMvc configuration (SpringMockMvcConfiguration
), so a full fledged application context will be available. In order to restrict the amount of Beans in the test, (a) specific Test WebMvc configuration(s) could be used. This kind of configuration should only be considered when the desired behavior is to perform tests with a considerable degree of integration between your components. The next section describes standalone setup, which should be the preferred way when your tests focus only on the controller behavior.
MockMvc standalone setup
MockMvcBuilders
offers an additional way to setup MockMvc
, standalonSetup(...)
. This method allows us to register one or more Controllers without the need to use the full WebApplicationContext
. Mocking controller dependencies using this procedure is really easy, this enables us to test a controller isolated from its dependencies same as we did in the strict unit test example. With MockMvc
we are also able to test path, request method, headers, responses, etc. so we are getting the best of both worlds.
The following test shows this behavior:
1@RunWith(SpringJUnit4ClassRunner.class)
2@ContextConfiguration(classes = {
3 LanguageApiController.class
4})
5@Import(Config.class)
6public class LanguageApiControllerTest {
7
8 @Autowired
9 private LanguageService languageService;
10
11 @Autowired
12 private LanguageApiController languageApiController;
13
14 private MockMvc mockMvc;
15
16 @Before
17 public void setUp() {
18 mockMvc = MockMvcBuilders
19 .standaloneSetup(languageApiController)
20 .build();
21 }
22
23/* ... */
24
25 @TestConfiguration
26 protected static class Config {
27
28 @Bean
29 public LanguageService languageService() {
30 return Mockito.mock(LanguageService.class);
31 }
32
33 }
34
35}
The configuration differs a little from the previous. @WebAppConfiguration
annotation is no longer used, so Spring won’t load a full application context (less overhead). The @ContextConfiguration
annotation includes a reference to the real controller we want to test, Spring will create a Bean for this controller and will autowire it to the test. Finally there is an inner configuration where we declare mock beans for the dependencies of the controller. Spring will inject this mock instances to our real controller. These dependencies will also be autowired to our test, so preparing mock data will be really easy.
Languages Happy Path
The following test uses the standalone MockMvc
configuration and describes the happy path of the /api/languages
endpoint:
1@Test
2public void getLanguages_null_shouldReturnOk() throws Exception {
3 // Given
4 final Language mockedEsoteric = new Language("Arnoldc", "Lauri Hartikka");
5 final List<Language> mockedLanguages = Stream.concat(
6 LanguageRepository.LANGUAGES.stream(),
7 Stream.of(mockedEsoteric)).collect(Collectors.toList());
8 doReturn(mockedLanguages).when(languageService).getLanguages(null);
9
10 // When
11 final ResultActions result = mockMvc.perform(
12 get("/api/languages")
13 .accept(MimeTypeUtils.APPLICATION_JSON_VALUE));
14
15 // Then
16 result.andExpect(status().isOk());
17 result.andExpect(jsonPath("$.length()").value(mockedLanguages.size()));
18 result.andExpect(jsonPath("$[?(@.name === 'Arnoldc')]").exists());
19}
In the Given section we prepare a list of languages that will be returned by the mocked LanguageService
Bean when a null parameter is received by the getLanguages
function.
Next we perform the GET
request to our Mock Mvc environment using an Accept: application/json
header. Finally we assert that the request returns with the expected 200 status code and that languages include all of our mocked data in the expected JSON path.
As mentioned earlier, not only we are testing the controller implementation, but also content negotiation headers, JSON serialization and http response status code.
Invalid Accept header
The following test will highlight how using MockMvc
will assert that endpoint annotations work as expected validating that the endpoint is only exposed when receiving the expected Accept: application/json
request header.
1@Test
2public void getLanguages_invalidAcceptHeader_shouldReturnNotAcceptable() throws Exception {
3 // Given
4 final String invalidAcceptMimeType = MimeTypeUtils.APPLICATION_XML_VALUE;
5 doReturn(LanguageRepository.LANGUAGES).when(languageService).getLanguages(null);
6
7 // When
8 final ResultActions result = mockMvc.perform(
9 get("/api/languages")
10 .accept(invalidAcceptMimeType));
11
12 // Then
13 result.andExpect(status().isNotAcceptable());
14}
In this case, the request is performed using an Accept: application/xml
request header. The endpoint is configured to serve only JSON data, so Spring MVC will never invoke the endpoint and will return a 406 not acceptable response status code.
Testing the ExceptionHandler
The LanguageApiController
includes an @ExceptionHandler
that will handle controlled exceptions thrown anywhere in the Controller. When invoking the /api/languages/{name}
, if no language is found matching the path variable name a controlled SpringMockMvc
exception is thrown.
The following test will validate that the @ExceptionHandler
works as expected and that the controlled exception is serialized to a ResponseEntity
accordingly.
1@Test
2public void getLanguage_nonExistingLanguage_shouldReturnNotFound() throws Exception {
3 // Given
4 final String mockedLanguageName = "Arnoldc";
5 doReturn(Optional.empty()).when(languageService).getLanguage(mockedLanguageName);
6
7 // When
8 final ResultActions result = mockMvc.perform(
9 get("/api/languages/".concat(mockedLanguageName))
10 .accept(MimeTypeUtils.APPLICATION_JSON_VALUE));
11
12 // Then
13 result.andExpect(status().isNotFound());
14}
In the Given section we configure the mocked LanguageService
to return an empty Optional
when a language with the name “Arnoldc” is requested.
Next, a GET request to /api/languages/Arnoldc
is performed. Finally the test asserts that a 404 not found response status code is yielded according to the ...ResponseEntity.status(ex.getHttpStatus())...
statement in the ExceptionHandler
.
Conclusion
This post is an introduction to Spring MVC testing framework. It highlights the advantages of using MockMvc
instead of regular unit tests. We see three ways of setting up MockMvc
and how using standaloneSetup
has the advantages of unit and integration testing. The post focuses only on testing a basic spring REST controller, although MockMvc
can be used to test the complete suite of components from Spring MVC framework.
The full source code for this post can be found at Github.