Properly testing Spring MVC controllers
In this post I want to introduce you to the Spring MVC testing framework, a way to properly test your Spring MVC controllers.
The way I used to do it
I have always found unit testing Spring MVC controllers nearly useless because I felt they don’t test anything useful for the most part. If something would break in the implementation, those unit tests would rarely fail. To explain what I mean by this, let me give you an example. Say you want to unit test the following controller method:
<br> @RequestMapping(value = "/people/{groep}", method = RequestMethod.GET)<br> public String listPeopleInGroup(@PathVariable String group, ModelMap modelMap) {<br> List people = peopleService.listPeople(group);<br> modelMap.put("people", people);<br> return "peopleList";<br> }<br>
All this controller method does is take the “group” parameter from the URL and list all people who are in that group. But it has a bug in it I didn’t notice. Usually you would unit test this controller by just mocking the peopleService using EasyMock or Mockito, pass in a fake Model and off you go! This is what the unit test in that case typically would look like (using JUnit, Mockito and Hamcrest):
<br> public class PeopleControllerTest {</p> <p> @InjectMocks<br> PeopleController controller;</p> <p> @Mock<br> PeopleService mockPeopleService;</p> <p> @Before<br> public void setUp() throws Exception {<br> MockitoAnnotations.initMocks(this);<br> }</p> <p> @Test<br> public void testListPeopleInGroup() {<br> List expectedPeople = asList(new Person());<br> when(mockPeopleService.listPeople("group")).thenReturn(expectedPeople);</p> <p> ModelMap modelMap = new ModelMap();<br> String viewName = controller.listPeopleInGroup("group", modelMap);</p> <p> assertEquals("peopleList", viewName);<br> assertThat(modelMap, hasEntry("people", (Object) expectedPeople));<br> }<br> }<br>
Let’s run this unit test …
Green light! Wow that’s amazing, this controller works like a charm! But oh wait, let me try to run this in a browser now.
What did we test?
Hmm, ok something obviously went wrong here. Why did the unit test not cover this? Well, as I mentioned before: it’s useless! Because what did it test exactly?
- A call to the peopleService is made
- The return value from the peopleService is put inside the ModelMap
- The correct view name is returned
What didn’t we test?
Some examples of things the unit test didn’t cover:
- That it responds to the correct url
- That the PathVariable correctly takes the value from the “{group}” part in the url
- That the method accepts GET requests
- That any class-level annotations are also working as you expect
- That any handler interceptors are called before and/or after executing this method
- That any other methods in the controller are called beforehand, like those
annotated with @ModelAttribute or @InitBinder. - That binding from parameters to an object works the way you expect
- That any validation errors were registered by validators
Introducing the Spring MVC test framework
Recently a colleague of mine pointed me towards a little framework created as part of the Spring Framework called spring-test-mvc
. This framework makes unit testing controllers a lot more meaningful! It actually has the ability to test all the aspects of a controller method I couldn’t test before. Let’s see if I can reproduce the above bug using Spring Test MVC.
<br> public class PeopleControllerTest {</p> <p> @InjectMocks<br> PeopleController controller;</p> <p> @Mock<br> PeopleService mockPeopleService;</p> <p> @Mock<br> View mockView;</p> <p> MockMvc mockMvc;</p> <p> @Before<br> public void setUp() throws Exception {<br> MockitoAnnotations.initMocks(this);<br> mockMvc = standaloneSetup(controller)<br> .setSingleView(mockView)<br> .build();<br> }</p> <p> @Test<br> public void testListPeopleInGroup() throws Exception {<br> List expectedPeople = asList(new Person());<br> when(mockPeopleService.listPeople("someGroup")).thenReturn(expectedPeople);</p> <p> mockMvc.perform(get("/people/someGroup"))<br> .andExpect(status().isOk())<br> .andExpect(model().attribute("people", expectedPeople))<br> .andExpect(view().name("peopleList"));<br> }<br> }<br>
The above unit test almost needs no explanation. It reads like reading a book. We perform a get request, check if the status is OK (status 200), check if a model attribute exists named “people” and check if the view name is “peopleList”. Now let’s run this unit test …
Yes we did it! We are now essentially seeing the same error message as we saw in the browser. So what mistake could I have made in my controller method that a ‘normal’ unit test failed to cover? Let’s take another quick look at the code.
<br> @RequestMapping(value = "/people/{groep}", method = RequestMethod.GET)<br> public String listPeopleInGroup(@PathVariable String group, ModelMap modelMap)<br>
Oh yes, of course! I made a typo in the path variable inside the url string. Of course “groep” doesn’t match the method argument named “group” so the request fails. Let’s correct my mistake.
<br> @RequestMapping(value = "/people/{group}", method = RequestMethod.GET)<br> public String listPeopleInGroup(@PathVariable String group, ModelMap modelMap)<br>
Run it again …
This proves my point. A unit test written using Spring Test MVC closely resembles a real request made by a browser. That’s why this test is a lot more meaningful. The fact that this test is now passing makes it very likely that it will work in a browser as well. The fact that the other unit test passed, didn’t tell us anything.
Testing a REST interface
Spring Test MVC is especially useful when building a REST interface which for example returns JSON responses. The framework contains all sorts of stuff for easily building a test request and carefully examining the response.
Let’s say we have a controller that listens to the URL “/people” and adds a person to the database whenever a POST request was received on that URL. The request body would be a JSON representation of the person to add. The response body will hold JSON that tells us what the database identifier of the person is that was just added, and also gives us a list of all people that have been added so far. This is how you would test this:
<br> mockMvc.perform(post("/people")<br> .contentType(MediaType.APPLICATION_JSON)<br> .body("{\"firstName\":\"Tom\", \"lastName\":\"van Zummeren\"}".getBytes()))<br> .andExpect(status().isCreated())<br> .andExpect(jsonPath("$.identifier", equalTo("123")))<br> .andExpect(jsonPath("$.allPeople[*].firstName", hasItem("Tom")));</p> <p>verify(mockPeopleService).persistPerson(new Person("Tom", "van Zummeren"));<br>
In this example we see a few new things. The content type is set on the request, a request body is given, and the response is checked using an expression language provided by the “json-path” framework.
This test tests so many things now. It’s not just calling the controller method, it’s also testing:
- The mapping of the request body to a Person object
- The status code that was set on the response
- That it supports the given content type
- That it listens to a POST on the /people URL
- The mapping of the response object is transformed correctly to JSON
Conclusion
Spring Test MVC is indispensable if you want to test your Spring MVC controllers. Simply testing the controller methods without including the Spring MVC framework itself, is useless. Spring Test MVC will be included in the Spring 3.2 release (so I’m told) but for now it can be found on Github: https://github.com/SpringSource/spring-test-mvc