Integration testing Spring MVC Annotated Controllers
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<string ,String> 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(); } } </string>
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.









These classes were EXACTLY what I was looking for.
Many thanks!
―DC
Thank you! This is a really great harness.
I’m running into a situation, however, where it seems as if the DefaultAnnotationHandlerMapping (when seen in debugger) appears to not have any handlers associated with it… even though my classes are annotated with RequestMappings. So while the Dispatcher appears to load up with my spring config, no handler resolves succesfully. Wondering if you ran into this. Perhaps I don’t understand something. A sample of test below…
@Test
public void resolvesUrlsCorrectly() throws Exception {
DispatcherServlet dispatcherServlet = getServletInstance();
MockHttpServletRequest request = new MockHttpServletRequest(“GET”, “/search”);
MockHttpServletResponse response = new MockHttpServletResponse();
dispatcherServlet.service(request, response); //throws ServletException, IOException
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsString()).isEqualTo(“TestSearchViewName”);
}
Again, thanks!
A few things to check if you run into this situation. One is to make sure that it’s loading your spring context and your spring servlet context.
Another thing is to check to see if the beans exist in the Context. You can do this by implementing an InitializingBean and looking inside the BeanFactory.
I am running into the same problem as KS.
Testing handler map [org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping@33f513] in DispatcherServlet with name ”
No handler mapping found for [/user]
No mapping found for HTTP request with URI [/user] in DispatcherServlet with name ”
I have output prior to this that indicates that both servlet context files are loading. It seems to load a broken context following this that perhaps overrides the working one? Either way I feel like I’m so close!
Thanks anyway if you can’t help, great article.
A quick final question:
new MockServletContext(“/WebContent” <<– what does this value relate to?
David,
the first parameter to
MockServletContextis the path that resources are available at within the WAR. In my test context, I have aWebContentdirectory, but you might not need it and should try just removing that property.As for your problem, does your console show whether or not it was able to load all servlet files?
I have managed to fix this problem by changing the following code:
GenericWebApplicationContext wac = new GenericWebApplicationContext();
wac.setParent(MockWebContextLoader.getInstance());
wac.registerBeanDefinition(“viewResolver”, new RootBeanDefinition(TestViewResolver.class));
wac.refresh();
return wac;
for:
return MockWebContextLoader.getInstance();
and then adding a content type header to the MockHttpServletRequest:
mockHttpServletRequest.addHeader(“Content-Type”, “application/json”);
WOW!!!
I have had this problem..
“java.lang.IllegalStateException: ApplicationEventMulticaster not initialized – call ‘refresh’ before multicasting events via the context”
But, It’s solved that problem by this post.
Thank U!!!!!!!!!!
Choonguri,
I’m glad I could be of help!
This is a nice post, the most informative I’ve been able to find. Bit of a problem though: When I run only the controller tests it works nicely. When run along with my dao and service layer integration tests, the controller test runs last and fails. Same ApplicationEventMulticaster not initialized as someone commented.
Those tests use the default context loader, not the MockWebContext loader:
@ContextConfiguration("/application-context.xml") @TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true) @TransactionalHave you setup tests across many layers?
Another thought is the nature of the TestViewResolver. Many people use coverage reports from tools like Clover. The tests you describe will execute a lot of code and jump the coverage numbers quite a bit. If the tests only assert the viewname and nothing else, that’s a good example of getting false confidence from the coverage results.
Its probably worthwhile to use the built in java xml dom classes to load the html from the view and assert a few basic things.
Ted,
Those are some good points that point out the limitations of this approach that I don’t have real solutions to.
The way I got around the issues with testing multiple layers is running tests for the other systems in a different JVM. Our DAO’s and service layers live in different projects from our controllers.
With the coverage, we just used a process solution and adjusted our targets in accordance with the extra executions. Our coverage numbers were mostly for us, so it fit our need.
We did investigate using a dom parser to check the output document, but we had a whole off-shore QA team that was doing only that. I’d be interested to see if there’s a good solution to work those tests in code, since each time I’ve approached the topic, it didn’t seem like it was worth the work.