July 28th, 2010

A while back I posted some thoughts on how to integration test Spring’s MVC annotation mapppings for controllers.  Since then I’ve developed my strategy a little further after find a few gaps in my original tests.

Integration testing interceptors and @PathVariable

The most noticeable problem with my original approach is that it doesn’t test any interceptors that are configured and this is something you probably want to include in your integration tests.  One unexpected (for me at least) side effect of this is that methods that include @PathVariable annotations on their parameters don’t work either.  You get the following exception:

org.springframework.web.bind.annotation.support.HandlerMethodInvocationException: Failed to invoke handler method [public org.springframework.web.servlet.ModelAndView test.MyClass.myMethod(test.SomeType)]; nested exception is java.lang.IllegalStateException: Could not find @PathVariable [parameterName] in @RequestMapping

This is because an interceptor is used by Spring to extract path variables from the request, before it hits the controller and processes the corresponding annotated parameters.

Use common handle method in integration tests

Using the same example class before:

@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";
    }
}

Here’s my updated implementation of an integration test. In it I define a handle method that is called by each test after it has configured the request to mimic that sent by the browser. This handle method includes logic to execute each of the interceptors configured for that request first, before passing control to the controller. It also makes no assumption about what class the controller is.

@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;
    
    @Before
    public void setUp() throws Exception {
        this.request = new MockHttpServletRequest();
        this.response = new MockHttpServletResponse();

        this.handlerAdapter = applicationContext.getBean(HandlerAdapter.class);
    }
        
    ModelAndView handle(HttpServletRequest request, HttpServletResponse response) 
            throws Exception {
        final HandlerMapping handlerMapping = applicationContext.getBean(HandlerMapping.class);                
        final HandlerExecutionChain handler = handlerMapping.getHandler(request);
        assertNotNull("No handler found for request, check you request mapping", handler);
        
        final Object controller = handler.getHandler();
        // if you want to override any injected attributes do it here

        final HandlerInterceptor[] interceptors = 
            handlerMapping.getHandler(request).getInterceptors();
        for (HandlerInterceptor interceptor : interceptors) {
            final boolean carryOn = interceptor.preHandle(request, response, controller);
            if (!carryOn) {
                return null;
            }
        }
        
        final ModelAndView mav = handlerAdapter.handle(request, response, controller);
        return mav;
    }
    
    @Test
    public void testNewForm() throws Exception {
        request.setMethod("GET");
        request.setRequestURI("/simple-form");

        final ModelAndView mav = handle(request, response);
        // make assertions on the ModelAndView here
    }

    @Test
    public void testProcessFormSubmission() throws Exception {
        request.setMethod("POST");
        request.setRequestURI("/simple-form");
        // set some request parameters for binding

        final ModelAndView mav = handle(request, response);
        // make assertions on the ModelAndView here plus any side effects
    }