REST webservices do not need integration tests


For REST webservices, integration tests have become superfluous. Not that testing them is not important (it is), but because we can fully test them using component tests.
This post continues our series to replace our Spring XML configuration with code and to fully test it and the code that uses it. As a finale, I’ll demonstrate how to test REST webservices during the unit/component test phase, thus reducing the amount of time spent testing.


Introduction




In the previous post in this series, you’we read how to use configuration profiles to unit test your controllers. If you’ve ever used custom conversions of inputs however, you’ll be fully aware of an important limitation of these unit tests: they do not test the interplay between conversions and controllers.



So in order to test how your controllers handle the converted input, you start your server and run some integration tests. As this test does a full cycle, you’re testing everything: from the conversions done by your framework to calling your controllers, and everything below that including the database. This has a disctinct disadvantage: it is (relatively) slow, and thus integration tests are generally not run nearly as often as the unit tests. This makes fixing bugs found during your integration tests more expensive as well.



But it does not have to be this way. In fact, when you’re using the Spring MVC web framework, it is possible to test the entire request to response cycle for your controllers. The only thing not tested are JSP pages, servlets and other resources served by your servlet container (or application server). View technologies like Velocity or Freemarker will be tested, provided they’re used in a ViewResolver.
This means that:


  • for REST webservices, and

  • for web applications that do not use JSP’s or Tiles,

  • where all database access is abstracted away using frameworks like JPA


integration testing is no longer necessary.



Let’s find out how we can do this.



 



Setting up spring-test-mvc




First, let’s grab the spring-test-mvc source code (the project is not yet incorporated in the SpringSource spring-test module, so there is no maven module yet). When you’ve got it, build it using mvn install (note: the normal mvn package or mvn verify do not install it in your local repository). Finally, add it as a test dependency to our REST webservice project.



spring-test-mvc is available for our tests now. But to use spring-test-mvc to test our controllers, we’ll need an application context. We’ll also want to make writing tests easier, so we’ll use a base class like this:



public class ControllerTestBase {

protected final MockMvc mockMvc;
protected final RequestResultMatchers requestResult = MockMvcResultActions.request();
protected final ResponseResultMatchers responseResult = MockMvcResultActions.response();
protected final ViewResultMatchers viewResult = MockMvcResultActions.view();
protected final ModelResultMatchers modelResult = MockMvcResultActions.model();
protected final ControllerResultMatchers controllerResult = MockMvcResultActions.controller();

protected ControllerTestBase() {
mockMvc = MockMvcBuilders.annotationConfigMvcSetup(ApplicationConfiguration.class, TestConfiguration.class).activateProfiles("testing").build();
}
}


In the example code above, the highlighted parts depend on the code base you’re testing. The two configuration classes contain the normal configuration and overrides from the testing configuration.



Now that we’re all set, let’s get to the interesting part: our application.



A first component test




To test our controllers, we’ll create a subclass of the base class defined above. Then, it’s a simple matter of defining the request, and matching the result:



public class MyControllerTest extends ControllerTestBase {

@Test
public void testFirstControllerMethod() {

Object valueSetByFilter = new Object();
String resultWithoutPrivacySensitiveFields = "...";
DefaultMockHttpServletRequestBuilder requestBuilder = MockHttpServletRequestBuilders.get("/path/to/record/23");
requestBuilder.accept(MediaType.APPLICATION_JSON).param("public", "true").requestAttr("SomeFilter.object", valueSetByFilter);
requestBuilder.locale(null).;
MockMvcResultActionHelper mvcResultActions = mockMvc.perform(requestBuilder);
// Test that the correct controller method was called.
mvcResultActions.andExpect(controllerResult.controllerType(ApplicationInfoController.class))
.andExpect(controllerResult.methodName("firstMethod"))
.andExpect(controllerResult.method(ApplicationInfoController.class, "firstMethod", Integer.class)); // This test tests more than the first two.
// Test what a REST client sees
mvcResultActions
.andExpect(responseResult.status(HttpServletResponse.SC_OK))
.andExpect(responseResult.contentType(MediaType.APPLICATION_JSON.toString()))
.andExpect(responseResult.body(resultWithoutPrivacySensitiveFields));
// Test what a JSP page might see
//mvcResultActions.andExpect(requestResult.requestAttributeValue("SomeFilter.object", valueSetByFilter))
// .andExpect(requestResult.requestAttributeValue("recordId", 23L))
// .andExpect(requestResult.requestAttributesPresent("recordName"))
// .andExpect(responseResult.forwardedUrl("/WEB-INF/views/MyControllerEdit.jsp"));
// Same tests, but before view resolving
//mvcResultActions.andExpect(viewResult.name("MyControllerEdit"))
// .andExpect(modelResult.modelAttribute("recordId", 23L))
// .andExpect(modelResult.modelAttributesPresent("recordName"));
}
}


Note that most tests are commented out: these tests are not useful in the context of a REST webservice, but can be useful if you’ve defined filters and/or are building a web application with JSP pages.



Subsequent tests work exactly the same way:



  1. You build a request with a path, and optionally set:

       
    • a body,

    •  
    • parameters,

    •  
    • headers (there are convenience methods for cookies, and the Accept & Content-Type headers, including the character encoding),

    •  
    • request attributes,

    •  
    • the locale and

    •  
    • the principal your servlet container authenticated


  2. You let the MockMVC instance perform the request. This performs all necessary conversions, calls interceptors, etc.



  3. Finally, you test the result. This can be:


       
    • Whether the right controller method was called (to test the @RequestMapping annotations)

    •  
    • Whether the response contains the correct status (& message), cookies, headers and body

    •  
    • Whether request/session attributes from a filter are untouched

    •  
    • Whether the right view is returned (for web applications)

    •  
    • Whether the right model contents are returned, and if there were binding errors (for web applications)



 



Conclusion




As you can see, it’s now very easy to build a request, and test the results from our controller. Not only that, you can test every aspect of the result: whether the right method was called, if the returned view is correct, and wether the contents of the model, request and response and correct. As this is including all conversions and interceptors, this means that integration tests are truly no longer needed as claimed in our introduction.

No comments:

Post a Comment