Spring MVC Test Framework
The Spring MVC Test framework provides first class JUnit support for testing client and server-side Spring MVC code through a fluent API. Typically it loads the actual Spring configuration through theTestContext framework and always uses the
DispatcherServlet
to process requests thus approximating full integration tests without requiring a running Servlet container.
Client-side tests are
RestTemplate
-based and allow tests for code that relies on the RestTemplate
without requiring a running server to respond to the requests.
Before Spring Framework 3.2, the most likely way to test a Spring MVC controller was to write a unit test that instantiates the controller, injects it with mock or stub dependencies, and then calls its methods directly, using a
MockHttpServletRequest
and MockHttpServletResponse
where necessary.
Although this is pretty easy to do, controllers have many annotations, and much remains untested. Request mappings, data binding, type conversion, and validation are just a few examples of what isn't tested. Furthermore, there are other types of annotated methods such as
@InitBinder
,@ModelAttribute
, and @ExceptionHandler
that get invoked as part of request processing.
The idea behind Spring MVC Test is to be able to re-write those controller tests by performing actual requests and generating responses, as they would be at runtime, along the way invoking controllers through the Spring MVC
DispatcherServlet
. Controllers can still be injected with mock dependencies, so tests can remain focused on the web layer.
Spring MVC Test builds on the familiar "mock" implementations of the Servlet API available in the
spring-test
module. This allows performing requests and generating responses without the need for running in a Servlet container. For the most part everything should work as it does at runtime with the exception of JSP rendering, which is not available outside a Servlet container. Furthermore, if you are familiar with how the MockHttpServletResponse
works, you'll know that forwards and redirects are not actually executed. Instead "forwarded" and "redirected" URLs are saved and can be asserted in tests. This means if you are using JSPs, you can verify the JSP page to which the request was forwarded.
All other means of rendering including
@ResponseBody
methods and View
types (besides JSPs) such as Freemarker, Velocity, Thymeleaf, and others for rendering HTML, JSON, XML, and so on should work as expected, and the response will contain the generated content.
Below is an example of a test requesting account information in JSON format:
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration("test-servlet-context.xml") public class ExampleTests { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); } @Test public void getAccount() throws Exception { this.mockMvc.perform(get("/accounts/1").accept(MediaType.parseMediaType("application/json;charset=UTF-8"))) .andExpect(status().isOk()) .andExpect(content().contentType("application/json")) .andExpect(jsonPath("$.name").value("Lee")); } }
The test relies on the
WebApplicationContext
support of the TestContext framework. It loads Spring configuration from an XML configuration file located in the same package as the test class (also supports JavaConfig) and injects the created WebApplicationContext
into the test so a MockMvc
instance can be created with it.
The
MockMvc
is then used to perform a request to "/accounts/1"
and verify the resulting response status is 200, the response content type is "application/json"
, and response content has a JSON property called "name" with the value "Lee". JSON content is inspected with the help of Jayway's JsonPath project. There are lots of other options for verifying the result of the performed request and those will be discussed later.
The fluent API in the example above requires a few static imports such as
MockMvcRequestBuilders.*
, MockMvcResultMatchers.*
, and MockMvcBuilders.*
. An easy way to find these classes is to search for types matching "MockMvc*". If using Eclipse, be sure to add them as "favorite static members" in the Eclipse preferences under Java -> Editor -> Content Assist -> Favorites. That will allow use of content assist after typing the first character of the static method name. Other IDEs (e.g. IntelliJ) may not require any additional configuration. Just check the support for code completion on static members.
The goal of server-side test setup is to create an instance of
MockMvc
that can be used to perform requests. There are two main options.
The first option is to point to Spring MVC configuration through the TestContext framework, which loads the Spring configuration and injects a
WebApplicationContext
into the test to use to create a MockMvc
:@RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration("my-servlet-context.xml") public class MyWebTests { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); } // ... }
The second option is to simply register a controller instance without loading any Spring configuration. Instead basic Spring MVC configuration suitable for testing annotated controllers is automatically created. The created configuration is comparable to that of the MVC JavaConfig (and the MVC namespace) and can be customized to a degree through builder-style methods:
public class MyWebTests { private MockMvc mockMvc; @Before public void setup() { this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build(); } // ... }
Which option should you use?
The "webAppContextSetup" loads the actual Spring MVC configuration resulting in a more complete integration test. Since the TestContext framework caches the loaded Spring configuration, it helps to keep tests running fast even as more tests get added. Furthermore, you can inject mock services into controllers through Spring configuration, in order to remain focused on testing the web layer. Here is an example of declaring a mock service with Mockito:
<bean id="accountService" class="org.mockito.Mockito" factory-method="mock"> <constructor-arg value="org.example.AccountService"/> </bean>
Then you can inject the mock service into the test in order set up and verify expectations:
@RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration("test-servlet-context.xml") public class AccountTests { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Autowired private AccountService accountService; // ... }
The "standaloneSetup" on the other hand is a little closer to a unit test. It tests one controller at a time, the controller can be injected with mock dependencies manually, and it doesn't involve loading Spring configuration. Such tests are more focused in style and make it easier to see which controller is being tested, whether any specific Spring MVC configuration is required to work, and so on. The "standaloneSetup" is also a very convenient way to write ad-hoc tests to verify some behavior or to debug an issue.
Just like with integration vs unit testing, there is no right or wrong answer. Using the "standaloneSetup" does imply the need for some additional "webAppContextSetup" tests to verify the Spring MVC configuration. Alternatively, you can decide write all tests with "webAppContextSetup" and always test against actual Spring MVC configuration.
To perform requests, use the appropriate HTTP method and additional builder-style methods corresponding to properties of
MockHttpServletRequest
. For example:mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON));
In addition to all the HTTP methods, you can also perform file upload requests, which internally creates an instance of
MockMultipartHttpServletRequest
:mockMvc.perform(fileUpload("/doc").file("a1", "ABC".getBytes("UTF-8")));
Query string parameters can be specified in the URI template:
mockMvc.perform(get("/hotels?foo={foo}", "bar"));
Or by adding Servlet request parameters:
mockMvc.perform(get("/hotels").param("foo", "bar"));
If application code relies on Servlet request parameters, and doesn't check the query string, as is most often the case, then it doesn't matter how parameters are added. Keep in mind though that parameters provided in the URI template will be decoded while parameters provided through the
param(...)
method are expected to be decoded.
In most cases it's preferable to leave out the context path and the Servlet path from the request URI. If you must test with the full request URI, be sure to set the
contextPath
and servletPath
accordingly so that request mappings will work:mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main"))
Looking at the above example, it would be cumbersome to set the contextPath and servletPath with every performed request. That's why you can define default request properties when building the
MockMvc
:public class MyWebTests { private MockMvc mockMvc; @Before public void setup() { mockMvc = standaloneSetup(new AccountController()) .defaultRequest(get("/") .contextPath("/app").servletPath("/main") .accept(MediaType.APPLICATION_JSON).build(); } }
The above properties will apply to every request performed through the
MockMvc
. If the same property is also specified on a given request, it will override the default value. That is why, the HTTP method and URI don't matter, when setting default request properties, since they must be specified on every request.
Expectations can be defined by appending one or more
.andExpect(..)
after call to perform the request:mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());
MockMvcResultMatchers.*
defines a number of static members, some of which return types with additional methods, for asserting the result of the performed request. The assertions fall in two general categories.
The first category of assertions verify properties of the response, i.e the response status, headers, and content. Those are the most important things to test for.
The second category of assertions go beyond the response, and allow inspecting Spring MVC specific constructs such as which controller method processed the request, whether an exception was raised and handled, what the content of the model is, what view was selected, what flash attributes were added, and so on. It is also possible to verify Servlet specific constructs such as request and session attributes. The following test asserts that binding/validation failed:
mockMvc.perform(post("/persons")) .andExpect(status().isOk()) .andExpect(model().attributeHasErrors("person"));
Many times when writing tests, it's useful to dump the result of the performed request. This can be done as follows, where
print()
is a static import fromMockMvcResultHandlers
:mockMvc.perform(post("/persons")) .andDo(print()) .andExpect(status().isOk()) .andExpect(model().attributeHasErrors("person"));
As long as request processing causes an unhandled exception, the
print()
method will print all the available result data to System.out
.
In some cases, you may want to get direct access to the result and verify something that cannot be verified otherwise. This can be done by appending
.andReturn()
at the end after all expectations:MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn(); // ...
When all tests repeat the same expectations, you can define the common expectations once when building the
MockMvc
:standaloneSetup(new SimpleController()) .alwaysExpect(status().isOk()) .alwaysExpect(content().contentType("application/json;charset=UTF-8")) .build()
Note that the expectation is always applied and cannot be overridden without creating a separate
MockMvc
instance.
When JSON response content contains hypermedia links created with Spring HATEOAS, the resulting links can be verified:
mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people"));
When XML response content contains hypermedia links created with Spring HATEOAS, the resulting links can be verified:
Map<String, String> ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom"); mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML)) .andExpect(xpath("/person/ns:link[@rel='self']/@href", ns).string("http://localhost:8080/people"));
When setting up a
MockMvc
, you can register one or more Filter
instances:mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();
Registered filters will be invoked through
MockFilterChain
from spring-test
and the last filter will delegates to the DispatcherServlet
.
The framework's own tests include many sample tests intended to demonstrate how to use Spring MVC Test. Browse these examples for further ideas. Also the spring-mvc-showcase has full test coverage based on Spring MVC Test.
Client-side tests are for code using the
RestTemplate
. The goal is to define expected requests and provide "stub" responses:RestTemplate restTemplate = new RestTemplate(); MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate); mockServer.expect(requestTo("/greeting")).andRespond(withSuccess("Hello world", "text/plain")); // use RestTemplate ... mockServer.verify();
In the above example,
MockRestServiceServer
-- the central class for client-side REST tests -- configures the RestTemplate
with a customClientHttpRequestFactory
that asserts actual requests against expectations and returns "stub" responses. In this case we expect a single request to "/greeting" and want to return a 200 response with "text/plain" content. We could define as many additional requests and stub responses as necessary.
Once expected requests and stub responses have been defined, the
RestTemplate
can be used in client-side code as usual. At the end of the testsmockServer.verify()
can be used to verify that all expected requests were performed.
Just like with server-side tests, the fluent API for client-side tests requires a few static imports. Those are easy to find by searching "MockRest*". Eclipse users should add
"MockRestRequestMatchers.*"
and "MockRestResponseCreators.*"
as "favorite static members" in the Eclipse preferences under Java -> Editor -> Content Assist -> Favorites. That allows using content assist after typing the first character of the static method name. Other IDEs (e.g. IntelliJ) may not require any additional configuration. Just check the support for code completion on static members.
Spring MVC Test's own tests include example tests of client-side REST tests.
No comments:
Post a Comment