Annotations and POJO controllers make dead simple to unit test the web layer and ensure that the logic within it is correct. What’s not as clear is how to quickly (and automatically) test the configurations of your controllers and ensure the correct controller method is called with the correct parameters on a request.
After looking through the Spring MVC tests, it becomes apparent that you want to create a DispatcherServlet
and send it requests. If the DispatcherServlet
is initialized with the correct context, it will then behave just as it does in your web container. It will look at the request, find the correct handler, and make the appropriate controller call. I created three classes to help set up the environment.
public class MockWebContextLoader extends AbstractContextLoader {
public static final ServletContext SERVLET_CONTEXT = new MockServletContext("/WebContent", new FileSystemResourceLoader());
private final static GenericWebApplicationContext webContext = new GenericWebApplicationContext();
protected BeanDefinitionReader createBeanDefinitionReader(final GenericApplicationContext context) {
return new XmlBeanDefinitionReader(context);
}
public final ConfigurableApplicationContext loadContext(final String... locations) throws Exception {
SERVLET_CONTEXT.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, webContext);
webContext.setServletContext(SERVLET_CONTEXT);
createBeanDefinitionReader(webContext).loadBeanDefinitions(locations);
AnnotationConfigUtils.registerAnnotationConfigProcessors(webContext);
webContext.refresh();
webContext.registerShutdownHook();
return webContext;
}
public static WebApplicationContext getInstance() {
return webContext;
}
protected String getResourceSuffix() {
return "-context.xml";
}
}
The MockWebContextLoader
loads in the spring config locations and creates a WebContext
. In order for this environment to mimic the one in your web container, you will need to pass in the same configs. I will show you how to do that later.
To help validate the success of a test, I’ve created another ViewResolver
that just echoes the viewname
into the response. You could have the ViewResolver
return the correct view, but parsing that to gauge success seemed like too much of a headache.
public class TestViewResolver implements ViewResolver {
public View resolveViewName(final String viewName, Locale locale) throws Exception {
return new View() {
public String getContentType() {
return null;
}
@SuppressWarnings({"unchecked"})
public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().write(viewName);
}
};
}
}
Finally, I created an abstract
class that would handle the creation of the DispatcherServlet
for all tests that extend it. I’ve put my spring configuration files on the classpath
that my test run it. If your configs are elsewhere, you will need to modify MockWebContextLoader
to look in a different path.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader=MockWebContextLoader.class, locations={"/classes/spring.xml", "/springmvc-servlet.xml"})
public abstract class AbstractControllerTestSupport {
private static DispatcherServlet dispatcherServlet;
@SuppressWarnings("serial")
public static DispatcherServlet getServletInstance() {
try {
if(null == dispatcherServlet) {
dispatcherServlet = new DispatcherServlet() {
protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) {
GenericWebApplicationContext wac = new GenericWebApplicationContext();
wac.setParent(MockWebContextLoader.getInstance());
wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(TestViewResolver.class));
wac.refresh();
return wac;
}
};
dispatcherServlet.init(new MockServletConfig());
}
} catch(Throwable t) {
Assert.fail("Unable to create a dispatcher servlet: " + t.getMessage());
}
return dispatcherServlet;
}
protected MockHttpServletRequest mockRequest(String method, String uri, Map params) {
MockHttpServletRequest req = new MockHttpServletRequest(method, uri);
for(String key : params.keySet()) {
req.addParameter(key, params.get(key));
}
return req;
}
protected MockHttpServletResponse mockResponse() {
return new MockHttpServletResponse();
}
}
With this harness, it’s trivial to automatically check the configuration of the web controllers. Since the functionality is tested separately from the configuration, the problems can be isolated quicker, leaving more time to fix them.