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
}