The future of Web MVC testing
Wednesday, December 19, 2012 at 6:12PM Ok, ok, I'm excited. Right now I just was able to hack my way through a test with Spring 3.2 and the new test web context.
The method I ended up writing looked like this:
@Test
public void tryStartQuiz() throws Exception {
this.mvc.perform(get("/engine/start/james"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.quiz_id").exists());
}
How did I get here?
I upgraded to Spring 3.2, which has native support for loading Web contexts (well, not in a separate-but-parent/child way where you can mount the web context and business context separately, that's coming, but in one context).
Here is the top of my test class:
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
"file:src/main/webapp/WEB-INF/spring/webmvc-config.xml",
"classpath:/META-INF/spring/applicationContext.xml"})
public class EngineControllerTest {
@Autowired
WebApplicationContext context;
MockMvc mvc;
@Before
public void setUp() {
mvc = webAppContextSetup(this.context).build();
}
...
Some analysis...
The MockMvc object, which I referred to as this.mvc in my test, allows me to mock up the web container request and record expectations. It is created in the @Before method, after Spring injects a test WebApplicationContext object, which is driven via the @WebAppConfiguration above.
The WebApplicationContext object is a web app engine in a test engine. It boots a Spring MVC platform, mocking the network engine so that you can submit requests with calls like mvc.perform(get(...) ... and chain assertions, using .andExpect statements to build conditions. It's like headless Spring MVC. This allows you to test by submitting URLs but without having to configure Jetty or Tomcat in a complex server bootstrapped integration test.
But where is it looking for my webapp?
As a nice touch, the default path for the web application is src/main/webapp. If you're not mavenized, or have a test version of your webapp you want to use, you can submit the path to it as the value of the annotation, like:
@WebApplicationContext("src/test/lightweightwebapp")
Assertions and matchers
There are several static imports that make life easier, as outlined in this great slide show from the MVC team (Rossen and friends):
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
I had to hack a bit with the commands and don't have the DSL figured out much so far. But it is nice to test through the request dispatcher rather than inject a controller directly. It's more natural and lets you really test the way production will handle your requests/responses.
What about more sophisticated checks?
The expect syntax doesn't really allow dicing and slicing of the response as easily as grabbing it in a variable. You can tell the mock MVC object to return the result, which contains the MockHttpServletResponse. In this example, I'm using FlexJson (from Roo's dependencies) to deserialize the JSON request into a HashMap of string keys and values, and assert that the data exists in the proper format.
@Test
public void tryStartQuiz() throws Exception {
JSONDeserializer<HashMap<String, String>> deserializer
= new JSONDeserializer<HashMap<String, String>>();
MvcResult result = this.mvc.perform(get("/engine/start/james"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.quiz_id").exists())
.andReturn();
Map<String, String> jsonPayload =
deserializer.deserialize(
result.getResponse().getContentAsString());
assertThat(jsonPayload.containsKey("quiz_id"), is(true));
assertThat(jsonPayload.get("nickname"), is("james"));
System.err.println(jsonPayload);
}
Roo and Spring 3.2
One more thing - I came from Roo 1.2.2, and as I upgraded the spring.version property to Spring 3.2 (yes, it works just fine) so I had to add one dependency in order for this to work, test scoped:
<dependency> <groupId>com.jayway.jsonpath</groupId> <artifactId>json-path</artifactId> <version>0.8.1</version> <scope>test</scope> </dependency>
Roo was just upgraded to version 1.2.3 this week (check it out here). I am hoping it uses Spring 3.2 out of the box.
This test will be checked into the next update of Quizzo-Angular-Roo - my current obsession which is using Spring Roo on the backend, and Angular.JS on the front-end, for a single-page JS quiz engine.
Where to go to view the code?
Today I've pushed this code (somewhat modified) along with working Jasmine tests, AND a working Maven test suite that includes automatic Jasmine testing every time I run mvn test. Check it out at quizzo-angular-roo on GitHub. I warn you, it will be primitive and so far not really doing much, but it will shape up over time.
spring,
spring mvc,
testing in
Tips
Reader Comments