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
    }

3 Responses to “More on integration testing of Spring’s MVC annotation mapppings for controllers”

  1. Richard says:

    I can’t get this to run without errors.

    org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [org.springframework.web.servlet.HandlerAdapter] is defined: expected single bean but found 0:

    I’m using it against my own controller and used @Autowired instead of @Inject on my applicationContext. I have changed my context configuration to:

    @ContextConfiguration(locations = { “classpath:applicationContextTest.xml” })

    Do I need to explicitly define a handleAdapter in my application context?

    Any help would be greatly appreciated. Thanks!

    • Anthony says:

      Richard,

      The error is saying that there is no MVC HandlerAdaptor bean present in your context. This is set-up by Spring MVC. Does your applicationContextTest.xml include a Spring MVC initialization?

      e.g. I use the mvc:annotation-driven element to do this.

  2. [...] codes I use here is largely from this blog post. I have to make a couple of minor changes to get it running in my development environment [...]

Leave a Reply

You must be logged in to post a comment.