Thursday, August 4, 2011

How to unit test Spring-based RESTful APIs

Even though the idea of the RESTful services is not new, REST APIs became popular only recently. There's not a single serious service without providing RESTful access to its resources. If there's any, then it's not serious...

There are many ways to implement a REST API: you can use different languages and can choose from multiple frameworks. Still being one of the most popular languages, my choice is Java with the Spring application framework. The annotation-based Spring controllers provide an elegant way to map the HTTP requests to services and then return a formatted response.

I had a problem, however, during the implementation. I love unit tests. I don't release software without testing it to its last bits. And those tests must be automatic, repeatable, fast, and natural to read. The only responsibilities of the controllers should be to map the HTTP requests to method calls and to translate the results into HTTP responses, but I couldn't find a way to test this most essential part of the REST API. At least not by googling for "how to unit test Spring controller request mapping". My research was not extensive, though, but none of the solutions and suggestions I found was to my liking. They were either not automatic, because a servlet container had to be started manually and the curl or wget commands were used later to try it out. Others required a lot of boilerplate code, for those started up an embedded Jetty, configured every HTTP request and then evaluated the HTTP responses. Yet another was just simply not easy enough to digest when I read it, because it used annotation-based test-configuration and relied on the ugly side effects of certain method calls.

After almost an hour of frustration I decided it is easier to think a bit harder than to find a solution I'd like, so I came up with my own that I'm going to share now. First we need a service with a REST API. For the sake of simplicity it'll be a feedback server where someone can leave a positive or a negative feedback - something similar to Facebook's Like and the missing Dislike buttons. An optional message can also be posted along with the feedback. Once a feedback is given, it can be viewed. I'll only show the relevant parts, but if you're interested, you can download the full source code from here. You won't find, however, a fancy client application with actual Like and Dislike buttons. Only the RESTful service is implemented which returns plain text or JSON to the caller. Here we go...

 @Controller
 public class FeedbackController {

     private final FeedbackService feedbackService;

     @Autowired
     public FeedbackController(FeedbackService feedbackService) {
         Validate.notNull(feedbackService, "feedbackService is required");

         this.feedbackService = feedbackService;
     }

     @RequestMapping(value = "/thumbsup", method = {RequestMethod.POST})
     public HttpEntity<String> saveThumbsUpFeedback(@RequestParam(value = "message", required = false) String message) {
         feedbackService.savePositiveFeedback(message);

         return new HttpEntity<String>("Thank you for your feedback", createHeader(TEXT_PLAIN));
     }

     @RequestMapping(value = "/thumbsdown", method = {RequestMethod.POST})
     public HttpEntity<String> saveThumbsDownFeedback(@RequestParam(value = "message", required = false) String message) {
         feedbackService.saveNegativeFeedback(message);

         return new HttpEntity<String>("Thank you for your feedback", createHeader(TEXT_PLAIN));
     }

     @RequestMapping(value = "/list", method = {RequestMethod.GET})
     public HttpEntity<String> listFeedbacks() {
         Gson gson = new GsonBuilder().setDateFormat("H:m:s dd:MM:yyyy").create();

         return new HttpEntity<String>(gson.toJson(feedbackService.listFeedbacks()), createHeader(APPLICATION_JSON));
     }

     private HttpHeaders createHeader(MediaType mediaType) {
         HttpHeaders httpHeaders = new HttpHeaders();
         httpHeaders.setContentType(mediaType);

         return httpHeaders;
     }

     @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
     public void handleRequestExceptions(HttpServletRequest request, HttpServletResponse response) throws IOException {
         response.sendError(SC_METHOD_NOT_ALLOWED, "Request method '" + request.getMethod()
         + "' is not supported on " + request.getRequestURI());
     }

     @ExceptionHandler
     public void handleExceptions(Exception e, HttpServletResponse response) throws IOException {
         response.sendError(SC_INTERNAL_SERVER_ERROR, "An internal error occurred.");
     }
 }

As you can see, the controller above is indeed quite simple:
  • when a POST request comes in to /thumbsup or /thumbsdown it instructs the feedback service to save the feedback
  • when a GET request comes in to /list it asks the feedback service to return every feedback and then converts them to JSON
  • if something goes wrong it handles the exceptions so that the client receives an HTTP status code along with a short message
I created this class with TDD in mind, so tests came first, then the implementation. The tests, however, were very useless: I didn't mock the FeedbackService class, so the tests only retested what was already tested. This was the moment when I started to look for solutions about testing the controller mappings. An hour later came the moment when I gave up and implemented what was necessary to test those nasty Spring annotations.

So what exactly do we need in our unit test? If you know Spring's Web MVC framework then you know that to make our controller work a DispatcherServlet has to be created in our web application's web.xml. This DispatcherServlet will then automatically take care of delegating the requests to their destinations. Simply put, we'll need a web application context and a DispatcherServlet instance in this context. Having configured everything, we can send mocked HTTP requests to the servlet which in turn will reply with mocked HTTP responses. Let's create our own test web application context with a DispatcherServlet:

 public class MockXmlWebApplicationContext extends XmlWebApplicationContext {

     public MockXmlWebApplicationContext(String webApplicationRootDirectory, String servletName, String... configLocations) throws ServletException {
         init(webApplicationRootDirectory, servletName, configLocations);
     }

     private void init(String webApplicationRootDirectory, String servletName, String... configLocations) throws ServletException {
         MockServletContext servletContext = new MockServletContext(webApplicationRootDirectory, new FileSystemResourceLoader());
         MockServletConfig servletConfig = new MockServletConfig(servletContext, servletName);
         servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this);

         DispatcherServlet dispatcherServlet = new DispatcherServlet();

         setServletConfig(servletConfig);
         setConfigLocations(configLocations);
         addBeanFactoryPostProcessor(new MockBeanFactoryPostProcessor(servletName, dispatcherServlet));
         addApplicationListener(new SourceFilteringListener(this, new ContextRefreshedEventListener(dispatcherServlet)));
         registerShutdownHook();
         refresh();

         dispatcherServlet.init(servletConfig);
     }

     private final class ContextRefreshedEventListener implements ApplicationListener<ContextRefreshedEvent> {

         private final DispatcherServlet dispatcherServlet;

         private ContextRefreshedEventListener(DispatcherServlet dispatcherServlet) {
             this.dispatcherServlet = dispatcherServlet;
         }

         @Override
         public void onApplicationEvent(ContextRefreshedEvent event) {
             dispatcherServlet.onApplicationEvent(event);
         }
     }

     private final class MockBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

         private final String servletName;
         private final DispatcherServlet dispatcherServlet;

         private MockBeanFactoryPostProcessor(String servletName, DispatcherServlet dispatcherServlet) {
             this.servletName = servletName;
             this.dispatcherServlet = dispatcherServlet;
         }

         @Override
         public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
             beanFactory.registerSingleton(servletName, dispatcherServlet);
         }
     }
 }

What's happening in this class? First of all it extends the XmlWebApplicationContext class which is needed if we want to simulate a servlet environment. Later in the init(String, String, String...) method we set up the servlet context and the servlet configuration. These will be used by the DispatcherServlet to find it's resources. If otherwise not specified, an XmlWebApplicationContext would try to load the standard applicationContext.xml from the WEB-INF directory. The applicationContext.xml, however, is not always in this directory or one may choose a different name for it, so it's configurable with the String... configLocations constructor argument. The locations listed in this argument are then used to load all the necessary configurations.

A DispatcherServlet is instantiated but it is not yet registered in the web application context. We can't simply add new beans to an existing application context - normally the beans are loaded from the applicationContext.xml - but we can register a post processor (a BeanFactoryPostProcessor) which will take care of it when the application context gets refreshed. The DispatcherServlet can receive events when the application context is refreshed so it's a good idea to set up an event listener (an ApplicationListener). Since the dispatcher servlet is only interested in the ContextRefreshedEvent, we can filter out the other application events by wrapping our event listener into a SourceFilteringListener, which will only delegate events that the listener can handle. An optional step is to register a shut down hook on our web application context so that the owned resources are properly released when the JVM is shut down. Having configured it all only two more steps are required: the application context should be refreshed so that the whole configuration gets processed and to initialize the dispatcher servlet.

From now on we can use the new MockXmlWebApplicationContext in our unit test with the proper arguments: we need to pass in the root directory of our web application, the name of the dispatcher servlet, and all the applicationContext.xml files which are needed by our application to work. Here is an example unit test demonstrating the usage of the web application context implemented above. The unit test checks if a POST request was sent to /thumbsup then a positive feedback is saved, and also checks if the request method is not supported on the accessed resource, the correct HTTP status code is set in the response along with an error message.

 public class FeedbackControllerTest {

     private static ApplicationContext applicationContext;

     private DispatcherServlet subject;

     @BeforeClass
     public static void setUpUnitTest() throws Exception {
         applicationContext = new MockXmlWebApplicationContext("src/main/webapp", "feedback-controller", "classpath:spring/applicationContext.xml");
     }

     @Before
     public void setUp() throws ServletException {
         subject = applicationContext.getBean(DispatcherServlet.class);
     }

     @Test
     public void shouldSavePositiveFeedbackWithoutMessage() throws Exception {
         MockHttpServletRequest request = new MockHttpServletRequest("POST", "/thumbsup");
         MockHttpServletResponse response = new MockHttpServletResponse();

         subject.service(request, response);

         assertThat(response.getStatus(), is(SC_OK));
         assertThat(response.getContentAsString(), is("Thank you for your feedback"));
         assertSavedFeedback(POSITIVE, null);
     }

     @Test
     public void shouldSetMethodNotAllowedStatusCodeIfPositiveFeedbackIsAccessedWithGet() throws Exception {
         MockHttpServletRequest request = new MockHttpServletRequest("GET", "/thumbsup");
         MockHttpServletResponse response = new MockHttpServletResponse();

         subject.service(request, response);

         assertThat(response.getStatus(), is(SC_METHOD_NOT_ALLOWED));
         assertThat(response.getErrorMessage(), is("Request method 'GET' is not supported on /thumbsup"));
     }
 }

The tests above meet all of my requirements. Not because I wrote them but because they satisfy the aforementioned criteria:
  • Being JUnit tests they can be automatically executed every time when the application is built. For the same reason they're also repeatable.
  • Other then loading the Spring configuration files by the MockXmlWebApplicationContext once in the beginning, the tests are executed very fast for they are not communicating with a real server.
  • The amount of boilerplate code is reduced to the short implementation of a web application context and to the straightforward configuration of this context. The web application context is reusable for every other controller, and there are no side effects utilized. No real server is started, no real requests are set up and sent to the server in every test, and no cumbersome parsing of HTTP responses are implemented, yet every bit of the controller is tested by simulating the communication between the client and the server.
  • The tests cases are short, descriptive, and are very natural to my eyes when I read them. I admit, this statement is subjective, but I still like that in only a few lines I set up an HTTP request, create an HTTP response object in which the result is expected, send the request to the dispatcher servlet, and then assert the response.

3 comments:

Anonymous said...

Excellent post, can i reuse this code ?

Szabolcs Andrási said...

Absolutely. Feel free to use it.

Anonymous said...

Excellent post.

Post a Comment