Sam Brannen and Rossen Stoyanchev presented ‘Spring 3.1 and MVC Testing Support’ at Spring 2GX back in October 2011. The idea is that you can write a unit test that accepts a Spring app context defining your Spring MVC components and be able to test your Controllers and all the Spring MVC ‘wiring’ by passing a mock request. The test itself does not have a reference to a Controller and thus you can simply send a request and receive a response. This type of test is very powerful in that you are testing Spring configuration, @RequestMapping annotations, controller behavior and view resolution.
Up to this point, we’ve tested Controllers only as plain vanilla unit tests which of course Spring recommends. Be sure to take a look at section 10.2 of http://static.springsource.org/spring/docs/3.1.x/spring-framework-reference/html/testing.html. This typically means instantiating the controller and invoking its methods directly, asserting on the response object and injecting mock objects for any dependencies. This is good but for the webapp under consideration we leave out all the view resolution which primarily consists of jaxb marshalling which can be quite particular when you mess up the xml annotations.
To supplement this style of testing and to test the app ‘end to end’, something Sam and Rossen alluded to in their presentation, my team has built a SoapUI test suite that would act as a client to the application (a REST app in this case). Of course the app needs to be built, deployed and running. Any integrated components like databases, web services, etc need to be up as well. The SoapUI tests are definitely a solid approach for testing your web app end to end but wouldn’t there be a lot of value in leveraging the Spring MVC Test Support?
Not only can I test the Controller behavior but whereas before I was asserting only on a response object, now I can assert on the actual response. Again, in this case, we’re talking xml documents (but obviously this can be applied to any response format or content type, json, html, etc …). There was one problem with this that I quickly realized when I started playing with this test framework. The framework attempts to resolve all @Autowired dependencies. What does this mean? Well, if you value a nice decoupled app layer architecture (and I know you do), your Controllers will have Services injected into them and your Services will have DAO’s injected into them. In our case, we have DAO objects that integrate with a database as well as SOAP web services. So I want to test my Controllers but I don’t want an end to end ‘integration test’ that tests the app through all its dependencies. That’s a bit too heavy and would be a nightmare for the continuous integration.
So what I need is to leverage the Spring MVC Test support framework but figure out how to short circuit those @Autowired dependencies and inject a mock in which I can control the behavior. In the end, it was a really simple thing to achieve but I will admit my colleague and I tossed it around a bit before figuring out the solution.
So what was the magic that we needed? We recognized the simple fact that in the tests we don’t have a reference to the Controller, hence, you can’t directly inject a mocked Service object. We also have to come up with a way for Spring not to chase down all those @Autowired dependencies.
Lets consider each separately though they are related. First off, how do I get a Mockito mock object into the app context? A little googling led to this post, injecting-mockito-mocks-into-a-spring-bean. So something like the following works:
<bean id="bookService" class="org.mockito.Mockito" factory-method="mock"> <constructor-arg value="com.test.service.book.BookService" /> </bean>
Interestingly, the post doesn’t discuss how to define behavior on the mock object which is something I quickly realized needed to be accomplished. Take a look at the following :
package com.test.controller.book; import static org.hamcrest.Matchers.equalTo; import static org.mockito.Mockito.when; import static org.springframework.test.web.server.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.server.result.MockMvcResultMatchers.content; import static org.springframework.test.web.server.result.MockMvcResultMatchers.status; import static org.springframework.test.web.server.result.MockMvcResultMatchers.xpath; import java.net.URI; import java.util.ArrayList; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.web.server.MockMvc; import org.springframework.test.web.server.RequestBuilder; import org.springframework.test.web.server.request.DefaultRequestBuilder; import org.springframework.test.web.server.samples.context.GenericWebXmlContextLoader; import org.springframework.test.web.server.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import com.test.bean.Book; import com.test.bean.Books; import com.test.service.book.BookService; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(loader = TestGenericWebXmlContextLoader.class, locations = { "classpath:applicationContext.xml", "classpath:mockitoTestContext.xml" }) public class BookControllerTestContextTests { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; private BookService bookService; @Before public void setup() { this.mockMvc = MockMvcBuilders.webApplicationContextSetup(this.wac) .build(); bookService = wac.getBean(BookService.class); Books books = new Books(); List bookList = new ArrayList(); Book book2 = new Book(); book2.setIsbn("0307408841"); book2.setTitle("In the Garden of Beasts: Love, Terror, and an American Family in Hitler's Berlin"); bookList.add(book2); books.setBooks(bookList); when(bookService.getBooks()).thenReturn(books); } @Test public void testGetBooks() throws Exception { mockMvc.perform(get("/books").header("foo","bar")) .andExpect(status().isOk()) .andExpect(content().type(MediaType.APPLICATION_XML)) .andExpect( xpath("/books/books/title") .string(equalTo("In the Garden of Beasts: Love, Terror, and an American Family in Hitler's Berlin"))); } } class TestGenericWebXmlContextLoader extends GenericWebXmlContextLoader { public TestGenericWebXmlContextLoader() { super("src/main/resources", false); } }
I can pull the mock object out of the application context using getBean(). From there, I can define the behavior on the mock object. This comes to an interesting point. There is at least some coupling going on at this point. The test is somewhat aware of the Service mock that will ultimately be invoked via the Controller. I imagine there may be a way to abstract this out, maybe to some parent test class that defines all the behaviors of all the service mock objects in the app. I’ve not yet gone to that level but its possible.
So now I have something a little bit stronger than my initial Controller unit tests. Now I can simply invoke mockMvc.perform(get(“/books”) and get back the full fledged xml response thats gone through jaxb marshalling and I can assert on the structure and content of the xml. In addition, I have assurance that all the Spring MVC infrastructure is in place and working. I can run it on the continuous build and get an assurance that no one has broken any Spring MVC annotations, JAXB annotations and any Spring MVC configurations in the app context.
The other extremely important thing to point out is that I in fact had to get rid of @Autowired on the controller and use @Resource.
Take a look at the tip in section 3.11.3 of http://static.springsource.org/spring/docs/2.5.x/reference/beans.html#beans-autowired-annotation.
The reason being is the Mockito mocked object is a proxied class of your Service interface. @Autowired resolves by type. @Resource resolves by bean name reference. This was just what was needed to resolve the (mock) Service dependency.
Take a look a the following links for reference. The framework is in github and not currently part of the Spring distribution.
https://github.com/SpringSource/spring-test-mvc/
http://rstoyanchev.github.com/spring-31-and-mvc-test/#1
I also posted the complete project at http://dl.dropbox.com/u/60625371/spring-mvc-sandbox.zip. The maven pom file has the dependency for spring-mvc-test along with the repository defined.
I look forward to having the Spring MVC testing support included in future versions of Spring and a special thanks to Sam and Rossen over at Springsource!