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.