March 16th, 2010

See also my follow up most: More on integration testing of Spring’s MVC annotation mapppings for controllers
I really love Spring 3’s MVC annotation based controllers. I was quite fond of the old style (and now largely deprecated) hierarchy of controllers provided by Spring 2: They removed the need for a lot of boiler plate code especially around parsing, validating and processing form submissions. However it always felt like you were building your controller implementations on top of a bit of a beast requiring knowledge of a long chain of events which you could hook into. With the annotation approach in Spring 3 my controllers feel light, less coupled and largely contain logic specific to my application.

As a very simple example.  Say you want a controller that processes a simple form.

@Controller
@RequestMapping("/simple-form")
public class MyController {
    private final static String FORM_VIEW = null;

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(String.class, new StringTrimmerEditor(false));
    }

    @RequestMapping(method = RequestMethod.GET)
    public MyForm newForm() {
        return new MyForm();
    }

    @RequestMapping(method = RequestMethod.POST)
    public String processFormSubmission(@Valid MyForm myForm,
            BindingResult result) {
        if (result.hasErrors()) {
            return FORM_VIEW;
        }
        // process the form
        return "success-view";
    }
}

This is great because:

  • The controller doesn’t extend a Spring class
  • You can call your methods that handle requests whatever you like (i.e. a name which best expresses their intention) with the @RequestMapping annotation reinforcing this view
  • There is virtually no boiler plate code
  • It’s integrated with the JSR 303 validation API
  • You decide what parameters go in the controller methods and Spring calls the method passing in the appropriate object

Confession: I haven’t been writing tests for the @RequestMapping, @RequestParam, @PathVariable and other annotations

Throughout the last two months of using the new features I’ve have had a nagging feeling that I haven’t been writing enough JUnit tests.  I’m very keen on writing tests and we use test-driven-development most of the time.  Unit testing the logic inside the controller methods is no issue.  In fact, that’s another great thing about Spring 3 controllers: they’re really easy to unit test.  We also continue to build up our bank of end to end regression tests using Selenium (excellent tool!).

What is missing are tests that ensure I’ve used the correct annotation in the correct place.  In the Spring reference manual tests of this nature are called integration tests and it talks about how you might want to write tests for Hibernate entity annotations and similar.  Looking into this I haven’t been able to find a satisfactory approach for testing the MVC annotations:  I’ve read suggestions to test a method is annotated with a particular annotation or to write functional tests with something like HtmlUnit.  What I want is something similar to the recommendation in the Spring documentation for writing database integration tests.

I’ve also read suggestions that I don’t need to test the annotations because Spring will have tests that already do that.  To me that argument is specious:  I’m not testing that the Spring annotations do what they’re supposed to, I’m testing that I’ve used the annotations correctly.

How to write integration tests for Spring MVC annotations

Here’s my approach to writing integration tests for Spring MVC annotations.  Taking the example above my code that I want to test would be for a HTTP GET:

  • results in a null view
  • a new command object being placed in the model

and a HTTP POST:

  • request parameters are bound to a command object
  • the parameter values are trimmed
  • the command object is validated and if not valid the errors are included in a binding result in the model and a null view returned

More generally I want to test that:

  • my Spring configuration files correctly configure the appropriate HandlerAdaptor
import static org.springframework.test.web.ModelAndViewAssert.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"file:web/WEB-INF/application-context.xml",
    "file:web/WEB-INF/dispatcher-servlet.xml"})
public class MyControllerIntegrationTest {

    @Inject
    private ApplicationContext applicationContext;

    private MockHttpServletRequest request;
    private MockHttpServletResponse response;
    private HandlerAdapter handlerAdapter;
    private MyController controller;

    @Before
    public void setUp() {
       request = new MockHttpServletRequest();
       response = new MockHttpServletResponse();
       handlerAdapter = applicationContext.getBean(HandlerAdapter.class);
       // I could get the controller from the context here
       controller = new MyController();
    }

    @Test
    public void testGet() throws Exception {
       request.setMethod("GET");
       final ModelAndView mav = handlerAdapter.handle(request, response, controller);
       assertViewName(mav, null);
       assertAndReturnModelAttributeOfType(mav, "myForm", MyForm.class);
    }

    @Test
    public void testPost() throws Exception {
       request.setMethod("POST");
       request.addParameter("firstName", "  Anthony  ");
       final ModelAndView mav = handlerAdapter.handle(request, response, controller);
       final MyForm myForm = assertAndReturnModelAttributeOfType(mav, "myForm", MyForm.class);
       assertEquals("Anthony", myForm.getFirstName());

       /* if myForm is not valid */
       assertViewName(mav, null);
       final BindingResult errors = assertAndReturnModelAttributeOfType(mav,
               "org.springframework.validation.BindingResult.myForm",
               BindingResult.class);
       assertTrue(errors.hasErrors());
    }
}

It should be fairly easy to extend this to more complex examples testing @RequestParam, @RequestMethod, @PathVariable, @RequestHeader@RequestBody.

What’s next

You might notice that I’m not testing the @RequestMapping("/simple-form") class level annotation in this test. I’ve not figured out what the best way to do this is just yet.