Spring Controller Tests 2.0
Spring has many tools in the toolbox to facilitate testing. However, when it comes to testing a Controller, the tools have been a bit blunt. To make life easier, Sam Brennan and Rossen Stoyanchev presented the spring-test-mvc framework during a session at SpringOne 2GX last year. It provides a Java DSL that enables white box Controller testing of request mappings, response codes, type conversions, redirects, view resolutions, and more. This post will briefly introduce how a Spring powered REST Controller test may be implemented when written as a standalone test with Mockito used as mocking framework.
Example Controller
Let’s assume that you have a simple UserController
class with just a single method:
@Controller
@RequestMapping("/user")
class UserController {
private final UserService userService;
@Autowired
UserController(UserService userService) {
this.userService = userService;
}
@RequestMapping(value = "/{userId}",
method = RequestMethod.GET,
produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
@ResponseBody
User getUser(@PathVariable("userId") long userId) {
return userService.getById(userId);
}
}
As can be seen, the getUser(long userId)
method is mapped to GET requests of the /user/{userId}
URL. When a request is executed, the method fetches information about a User
from the UserService
and serializes the result to JSON or XML depending on the accept header of the request.
Test
We start by verifying that we can fetch a user serialized to JSON. In plain English, we would like the following test: Perform a GET request to /user/0 with JSON as accepted media type, then expect the status code to be ok, the content type of the response to be of type JSON, the response body to have an expected “name” key/value pair and an expected “email” key/value pair. Compare with spring-mvc-test implementation below:
@Test
public void shouldGetTestUserAsJson() throws Exception {
mockMvc
.perform(get("/user/0")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("name", is("Test User")))
.andExpect(jsonPath("email", is("test.user@somewhere.com")));
}
Setup
In order for the test to work, we need to create a setup method:
@Before
public void setUp() {
userServiceMock = mock(UserService.class);
UserController userController = new UserController(userServiceMock);
testUser = new User("Test User", "test.user@somewhere.com");
when(userServiceMock.getById(0)).thenReturn(testUser);
mockMvc = MockMvcBuilders
.standaloneSetup(userController)
.build();
}
The first few lines should be familiar to all Mockito users. Basically, a UserService
instance is created by calling the Mockito.mock(Class classToMock) method. A testUser
instance is created, and the mock is instructed to return it. Next the UserController
to be tested is instantiated using the mock service. The last lines of the setup method contains the new MockMvc class. It is created by calling a factory method on the builder class MockMvcBuilders. In this example, we choose the standalone option to which the UserController
instance is passed. Alternatively, there are other factory methods available that creates a MockMvc
from a web application context, an XML application configuration or a Java based application configuration.
More tests
With some minor changes of the test method (change the media type to XML and use xpath instead of jsonPath to extract the response data), it can be verified that the Controller can return a user as an XML document:
@Test
public void shouldGetTestUserAsXml() throws Exception {
mockMvc
.perform(get("/user/0")
.accept(MediaType.APPLICATION_XML))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_XML))
.andExpect(xpath("/user/name").string("Test User"))
.andExpect(xpath("/user/email").string("test.user@somewhere.com"));
}
To make sure that clients cannot update a user, another test is created that verifies that POST is not permitted. (The allow header is part of the HTTP spec and added to the response header by Spring. Depending on your business case you may choose to ignore that expectation.):
@Test
public void shouldNotPostToUser() throws Exception {
mockMvc
.perform(post("/user/0"))
.andExpect(status().isMethodNotAllowed())
.andExpect(header()
.string("Allow", is("GET")));
}
Debugging
Sometimes it can be a bit hard to debug a test if an expectation fails. On those occasions, it can be beneficial to print some information to find out what is going on:
@Test
public void printInfo() throws Exception {
mockMvc
.perform(get("/user/0"))
.andDo(print());
}
When executed, the test prints information about the request and response to the console. Notably, it also prints information about handler type and handler methods. Due to the simple nature of this example, many of the lines are left blank, but it gives you some indication to what can be verified in other tests:
MockHttpServletRequest:
HTTP Method = GET
Request URI = /user/0
Parameters = {}
Headers = {}
Handler:
Type = com.jayway.controller.UserController
Method = com.jayway.domain.User com.jayway.controller.UserController.getUser(long)
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
MockHttpServletResponse:
Status = 200
Error message = null
Headers = {Content-Type=[application/json]}
Content type = application/json
Body = {"name":"Test User","email":"test.user@somewhere.com"}
Forwarded URL = null
Redirected URL = null
Cookies = []
For reference, click on the links to get the complete implementation of the UserControllerTest and the User class.
Dependencies
In the spring-mvc-test read-me file, it is stated that: This code will be included in the spring-test module of the Spring Framework. Consequently the project will be moved, hopefully as part of the upcoming Spring 3.2 release, and then the spring-test dependency can be used. However, currently there is a milestone release available in the Spring maven milestone repository. For the tests described above, you also need to add Mockito and JsonPath to your pom:
<repositories>
<repository>
<id>org.springframework.maven.milestone</id>
<name>Spring Framework Maven Milestone Repository</name>
<url>http://maven.springframework.org/milestone</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>3.2.0.RC1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>0.8.1</version>
<scope>test</scope>
</dependency>
<!-- More dependencies -->
</dependencies>
Alternatives
Another framework that partly solves the same problem is REST Assured. In comparison to the spring-mvc-test it offers a Java DSL for web based testing, but it focuses on integration testing of a running REST server, e.g. it is not possible to mock Controller dependencies or to validate specific Spring objects that are used internally by the application. On the other hand, it offers features like OAuth authentication and SSL support.
References
- Video recording of Sam Brennan’s and Rossen Stoyanchev’s session from with live IDE demos (or just the slides without demos).
- The spring-test-mvc project at GitHub.
- The spring-test-mvc samples at GitHub.
- Mockito
- The complete UserControllerTest.
Edit
Nov 12th, 2012: Updated code examples to reflect the changes in the Spring Framework 3.2 RC1 Release. For more information, read Rossen Stoyanchev’s blog post about the Spring MVC Test Framework.