Trifork Blog

Axon Framework, DDD, Microservices

Properly testing Spring MVC controllers

December 11th, 2012 by
|

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:

@RequestMapping(value = "/people/{groep}", method = RequestMethod.GET)
public String listPeopleInGroup(@PathVariable String group, ModelMap modelMap) {
    List<Person> people = peopleService.listPeople(group);
    modelMap.put("people", people);
    return "peopleList";
}

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):

public class PeopleControllerTest {

    @InjectMocks
    PeopleController controller;

    @Mock
    PeopleService mockPeopleService;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testListPeopleInGroup() {
        List<Person> expectedPeople = asList(new Person());
        when(mockPeopleService.listPeople("group")).thenReturn(expectedPeople);

        ModelMap modelMap = new ModelMap();
        String viewName = controller.listPeopleInGroup("group", modelMap);

        assertEquals("peopleList", viewName);
        assertThat(modelMap, hasEntry("people", (Object) expectedPeople));
    }
}

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.

public class PeopleControllerTest {

    @InjectMocks
    PeopleController controller;

    @Mock
    PeopleService mockPeopleService;

    @Mock
    View mockView;

    MockMvc mockMvc;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        mockMvc = standaloneSetup(controller)
                .setSingleView(mockView)
                .build();
    }

    @Test
    public void testListPeopleInGroup() throws Exception {
        List<Person> expectedPeople = asList(new Person());
        when(mockPeopleService.listPeople("someGroup")).thenReturn(expectedPeople);

        mockMvc.perform(get("/people/someGroup"))
                .andExpect(status().isOk())
                .andExpect(model().attribute("people", expectedPeople))
                .andExpect(view().name("peopleList"));
    }
}

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.

@RequestMapping(value = "/people/{groep}", method = RequestMethod.GET)
public String listPeopleInGroup(@PathVariable String group, ModelMap modelMap)

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.

@RequestMapping(value = "/people/{group}", method = RequestMethod.GET)
public String listPeopleInGroup(@PathVariable String group, ModelMap modelMap)

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:

mockMvc.perform(post("/people")
        .contentType(MediaType.APPLICATION_JSON)
        .body("{\"firstName\":\"Tom\", \"lastName\":\"van Zummeren\"}".getBytes()))
        .andExpect(status().isCreated())
        .andExpect(jsonPath("$.identifier", equalTo("123")))
        .andExpect(jsonPath("$.allPeople[*].firstName", hasItem("Tom")));

verify(mockPeopleService).persistPerson(new Person("Tom", "van Zummeren"));

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

12 Responses

  1. December 11, 2012 at 17:24 by Roberto van der Linden

    Nice one Tom! Will use it in my next Spring MVC project 🙂

  2. December 11, 2012 at 20:06 by Erik van Oosten

    Nice to see, in the first code example, that Spring also does translations from Dutch to English 😉

  3. December 11, 2012 at 20:08 by Erik van Oosten

    Ah, I see. Please remove that comment 🙂

  4. December 12, 2012 at 08:56 by Oliver Gierke

    The separate project has already been folded into the Spring Framework and included in the latest release candidates, so that it *will* be part of the GA release in a day or two.

  5. December 12, 2012 at 10:26 by Tom van Zummeren

    Hey Erik! Nice to know that you read my blog! Maybe the mistake I deliberately made in the code sample is too subtle… I should maybe make it more clear next time haha 🙂

  6. December 20, 2012 at 06:05 by Links for December 16th through December 19th

    […] Properly testing Spring MVC controllers – 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: […]

  7. April 21, 2013 at 20:20 by Java Spring Dev

    But what if I use annotation based configuration (@Configuration) for my web application? Can you describe how to set up unit tests in this case

  8. April 23, 2013 at 12:49 by Tom van Zummeren

    @Java Spring Dev: it does not matter how you defined your spring application context because the unit tests in my examples don’t use a spring application context at all! I only use Mockito to wire up the mocks (@Mock) with the controller (@InjectMocks).

  9. July 3, 2013 at 18:22 by Tarun

    I am using Spring 3.2 and the Data Binding from url encoded request params to the model object is not taking place.

  10. October 11, 2013 at 23:26 by will

    Hi Tom!
    I recently tried Spring MVC and wondered how to unit test controllers without involving selenium (which is the next step in the testing process of a webapp).
    You definitely gave us a cristal clear example and your explainations are very accurate. straight to the point!
    Thanks a lot for sharing!

  11. October 11, 2013 at 23:30 by will

    juste wondering:
    if you don’t use the spring context in your test, how does the autowiring work if i have an autowired service in my controller (yes, i’m quite new to spring 😉 )?

  12. April 6, 2015 at 22:50 by SM

    Not sure if I would classify this as a unit test. The failure that you mention when you test in the browser – that should be ideally be captured by an integration test. what you are saying here is if we do not invoke a method exactly as defined in code then that failure is not captured by my unit test. That would be a good test for integration efforts.