Unit Testing Spring MVC REST Controllers

Unit tests are supposed to run against functional methods.  The method under test is looked upon as a black box with inputs and outputs.  You define a set of inputs, both legal and illegal, and expect certain outputs or possible exceptions.  Ideally, you exercise all the different paths within the method.

With MVC controllers, there is automatic “wiring” that Spring sets up within the controller class.  The application server (such as tomcat), through the Spring interface, calls the appropriate controller method based on the input path specified in the HTTP request.  Furthermore, if the data package is a JSON string, the data is automatically converted to an appropriate Java object.  How do we implement a unit test for this?  Complete documentation for the Spring MVC Test Framework can be found here.

The example presented below uses the following library versions of applicable libraries:

Library Version
Spring 4.3.0
Mockito 2.0.78
Java 1.8
Jackson 2.7.2

The following pom.xml file is used to define the project:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>com.topshot.mvcdemo</groupId>
 <packaging>war</packaging>
 <version>1.0-SNAPSHOT</version>
 <artifactId>MVCDemo</artifactId>
 <name>MVC Demo App</name>
 <url>http://maven.apache.org</url>

 <properties>
   <javax.servlet.version>3.1.0</javax.servlet.version>
   <jackson.version>2.7.2</jackson.version>
   <jdk.version>1.8</jdk.version>
   <maven-compiler-plugin.version>2.3.2</maven-compiler-plugin.version>
   <maven-eclipse-plugin.version>2.9</maven-eclipse-plugin.version>
   <mockito.version>2.0.78-beta</mockito.version>
   <spring.version>4.3.0.RELEASE</spring.version>
 </properties>

 <dependencies>
   <!-- Spring dependencies -->
   <dependency>
     <groupId>org.springframework</groupId>
     <artifactId>spring-core</artifactId>
     <version>${spring.version}</version>
   </dependency>

   <dependency>
     <groupId>org.springframework</groupId>
     <artifactId>spring-web</artifactId>
     <version>${spring.version}</version>
   </dependency>

   <dependency>
     <groupId>org.springframework</groupId>
     <artifactId>spring-webmvc</artifactId>
     <version>${spring.version}</version>
   </dependency>

   <!-- Jackson JSON Mapper -->
   <dependency>
     <groupId>com.fasterxml.jackson.core</groupId>
     <artifactId>jackson-core</artifactId>
     <version>${jackson.version}</version>
   </dependency>
 
   <dependency>
     <groupId>com.fasterxml.jackson.core</groupId>
     <artifactId>jackson-databind</artifactId>
     <version>${jackson.version}</version>
   </dependency>
 
   <dependency>
     <groupId>com.fasterxml.jackson.core</groupId>
     <artifactId>jackson-annotations</artifactId>
     <version>${jackson.version}</version>
   </dependency>

   <dependency>
     <groupId>javax.servlet</groupId>
     <artifactId>javax.servlet-api</artifactId>
     <version>${javax.servlet.version}</version>
   </dependency>

   <dependency>
     <groupId>org.mockito</groupId>
     <artifactId>mockito-core</artifactId>
     <version>${mockito.version}</version>
   </dependency>

   <dependency>
     <groupId>junit</groupId>
     <artifactId>junit</artifactId>
     <version>4.12</version>
   </dependency>

   <dependency>
     <groupId>org.springframework</groupId>
     <artifactId>spring-test</artifactId>
     <version>${spring.version}</version>
   </dependency>

 </dependencies>

 <build>
   <finalName>MVCDemo</finalName>
   <plugins>
     <plugin>
       <groupId>org.apache.maven.plugins</groupId>
       <artifactId>maven-eclipse-plugin</artifactId>
       <version>${maven-eclipse-plugin.version}</version>
       <configuration>
         <downloadSources>true</downloadSources>
         <downloadJavadocs>false</downloadJavadocs>
         <wtpversion>2.0</wtpversion>
       </configuration>
     </plugin>
     <plugin>
       <groupId>org.apache.maven.plugins</groupId>
       <artifactId>maven-compiler-plugin</artifactId>
       <version>${maven-compiler-plugin.version}</version>
       <configuration>
         <source>${jdk.version}</source>
         <target>${jdk.version}</target>
       </configuration>
     </plugin>
   </plugins>
 </build>

</project>

Let’s start by looking at our controller class and see what it does.  The request that will initiate the call to getUsers is the path “/users” as seen on line 20 of MainController.java.  Data to be returned is an array of User objects in JSON format. The conversion of variable ‘users’ to JSON format is handled by Spring as a result of the two annotations, @RequestMapping and @ResponseBody (lines 20 and 21).

package com.topshot.mvcdemo.controller;

import java.util.Arrays;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.topshot.mvcdemo.model.User;
import com.topshot.mvcdemo.service.UserService;

@Controller
public class MainController {

   @Autowired
   private UserService userService;
 
   @RequestMapping(value = "/users", method = RequestMethod.GET,
                   produces="application/json")
   public @ResponseBody User[] getUsers() {
       User[] users = userService.getUsers();
       System.out.println("DEBUG users: " + Arrays.toString(users));
       return users;
   }

   @RequestMapping(value = "/user/{name}", method = RequestMethod.GET,  
                   produces="application/json")
   public @ResponseBody String getUser(@PathVariable String name) {
       return "The name is " + name;
   }
}

There are two ways we might want to unit test this code.  Much depends on the real implementation of the UserService getUsers() call within the outer getUsers() call.  If, as it is likely, userService.getUsers() calls a real database or some other service that needs to be running, then this is not really a unit test, rather it is an integration test that requires external setup to be functional. In this case we would want to mock up the userService.getUsers() call.  On the other hand, if this method call returns some kind of static information (perhaps from a flat file), then we can call the real method with our unit test.  I will show how both types of unit tests can be implemented.

In our first example, we will exercise MainController.getUsers() leaving the call to userService.getUsers() alone (i.e., we will make a real call to this method).  Here is the code for the unit test class:

package com.topshot.mvcdemo.controller;

import static org.junit.Assert.*;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

 // 1
 @RunWith(SpringJUnit4ClassRunner.class)
 @ContextConfiguration(locations = {"classpath:/mvc-dispatcher-servlet.xml"})
 @WebAppConfiguration
 public class MainControllerTest {

   // 2
   @Autowired 
   WebApplicationContext wac;
 
   private MockMvc mockMvc;
 
   @Before
   public void setUp() throws Exception {
     // 3
     mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
   }

   @Test
   public void testGetUsers() throws Exception {
     // 4
     RequestBuilder requestBuilder = 
       MockMvcRequestBuilders.get("/users").accept(new 
          MediaType("application", "json"));
     // 5
     ResultActions result = mockMvc.perform(requestBuilder);
     result.andExpect(MockMvcResultMatchers.status().isOk());
     String contentType = 
       result.andReturn().getResponse().getContentType();
     // 6
     assertTrue("contentType returned is incorrect.", 
     contentType.contains("application/json"));
     // 7
     String jsonData = 
       result.andReturn().getResponse().getContentAsString();
     assertNotNull(jsonData, "No data was returned.");
     assertTrue("Insufficent number of characters returned as data.", 
       jsonData.length() > 2);
     assertTrue("This JSON string is supposed to start with '['", 
       jsonData.startsWith("["));
     assertTrue("This JSON string is supposed to end with ']'", 
       jsonData.endsWith("]"));
   }

   @Test
   public void testBadPath() throws Exception {
     // 8 
     RequestBuilder requestBuilder = 
       MockMvcRequestBuilders.get("/Users").accept(new 
       MediaType("application", "json"));
     // 9
     ResultActions result = mockMvc.perform(requestBuilder);
     result.andExpect(
       MockMvcResultMatchers.status().is4xxClientError());
   }

   @Test
   public void testGetUser() throws Exception {
     RequestBuilder requestBuilder =    
       MockMvcRequestBuilders.get("/user/Dracula")
         .accept(new MediaType("application", "json"));

     ResultActions result = mockMvc.perform(requestBuilder);
     result.andExpect(MockMvcResultMatchers.status().isOk());
     String contentType = 
        result.andReturn().getResponse().getContentType();

     assertTrue("contentType returned is incorrect.",  
        contentType.contains("application/json"));

     String jsonData =   
        result.andReturn().getResponse().getContentAsString();
     assertNotNull(jsonData, "No data was returned.");
     assertTrue("Incorrect response", 
        jsonData.equals("The name is Dracula"));
 }

}

The following are comments associated with the above unit test Java code:

Note 1: These annotations define the wiring harness required to integrate JUnit with Spring MVC.  In particular, class SpringJUnit4ClassRunner provides the interface to the Spring TestContext framework.  The file mvc-dispatcher-servlet.xml defines the MainController bean and the UserService bean used by MainController.java (contents is shown below) via the @Autowired annotation.
Note 2: The WebApplicationContext is used in conjunction with the @WebAppConfiguration annotation to define the controller context. Using variable wac, one can retrieve the servlet context as well as other controller related information. Variable mockMvc is the main entry point for the server-side Spring MVC test support framework.
Note 3: This is standard setup code for initializing the mockMvc variable.
Note 4: The request to the getUsers service is built (but not executed) at this point. The HTTP path specified is “/users” and the data to be returned must be JSON data.
Note 5: It is here that the request is executed with the results being returned in the ResultActions variable, result. 
Note 6: Be careful about using String.equals instead of String contains.  In my own example, “application/json;charset=UTF-8” was actually returned in variable contentType.
Note 7: Here we verify the content returned from the server. This can be done many ways.
Note 8: Note that the RESTful path we are using has an upper-case ‘U’ for /users.  We don’t have such a service defined.
Note 9: An HTTP 404 error should be returned here.

This is the contents of file mvc-dispatcher-servlet.xml:

<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:context="http://www.springframework.org/schema/context"
 xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="
 http://www.springframework.org/schema/beans 
 http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
 http://www.springframework.org/schema/context 
 http://www.springframework.org/schema/context/spring-context-4.3.xsd
 http://www.springframework.org/schema/mvc
 http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd">

 <context:component-scan base-package=
       "com.topshot.proximity.controller" />

 <mvc:annotation-driven />

 <bean id="userService"   
       class="com.topshot.mvcdemo.service.UserService">
 </bean>
 
</beans>

Let’s modify our unit test somewhat. Instead of having MainController.getUsers() call the real UserService, we will create a mockup of that service using Mockito.  Our new unit test code will look like this:

 
package com.topshot.mvcdemo.controller;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import com.topshot.mvcdemo.model.User;
import com.topshot.mvcdemo.service.UserService;

 @RunWith(SpringJUnit4ClassRunner.class)
 @ContextConfiguration(
     locations = {"classpath:/mvc-dispatcher-servlet.xml"})
 @WebAppConfiguration
 public class MainControllerTest {

   // 1
   @InjectMocks
   private MainController mainController;
 
   @Mock
   private UserService userService;

   private MockMvc mockMvc;
 
   // dummy data
   public User [] getUsers() {
     User[] users = {
       new User("Bob Dylan"), 
       new User("Leonard Cohen")
     };
     return users;
   }
 
   @Before
   public void setUp() throws Exception {
     // 2
     MockitoAnnotations.initMocks(this);
     mockMvc = MockMvcBuilders.standaloneSetup(mainController).build();
   }

   @Test
   public void testGetUsers() throws Exception {
     // 3
     when(userService.getUsers()).thenReturn(getUsers());

     RequestBuilder requestBuilder =  
       MockMvcRequestBuilders.get("/users").accept(new 
       MediaType("application", "json"));

     ResultActions result = mockMvc.perform(requestBuilder);
     result.andExpect(MockMvcResultMatchers.status().isOk());
     String contentType = 
       result.andReturn().getResponse().getContentType();
 
     assertTrue("contentType returned is incorrect.", 
     contentType.contains("application/json"));
 
     String jsonData = 
       result.andReturn().getResponse().getContentAsString();
     assertNotNull(jsonData, "No data was returned.");
     assertTrue("Insufficent number of characters returned as data.", 
       jsonData.length() > 2);
     assertTrue("This JSON string is supposed to start with '['", 
       jsonData.startsWith("["));
     assertTrue("This JSON string is supposed to end with ']'", 
       jsonData.endsWith("]"));
     }
 }

The following comments point to changes in this copy of the Java unit test code when compared to the original unit test code:

Note 1: Annotation @InjectMocks is used by Mockito to define the Mock injections in the MainController class instance for constructors and methods. Using this annotation simplifies the amount of required test code. See the documentation for further information about this class. 
Note 2: Method MockitoAnnotations.initMocks() does the actual initialization of the Mock objects.
Note 3: This is a standard Mockito when() method.  Whenever a call is made to UserService.getUsers(), this code is executed instead of the real code.  

File mvc-dispatcher-servlet.xml can also be simplified.  No bean needs to be specified.  For the sake of completeness, I’m including the code for User and UserService:

User.java:

package com.topshot.mvcdemo.model;

public class User {

 String name;

 public User(String name) {
   this.name = name;
 }
 
 public String getName() {
   return name;
 }

 public void setName(String name) {
   this.name = name;
 }
}

UserService.java:

package com.topshot.mvcdemo.service;

import com.topshot.mvcdemo.model.User;

public class UserService {

   User[] people = {
     new User("Barak Obama"), 
     new User("George Bush"),
     new User("Bill Clinton"),
     new User("Abraham Lincoln")
   }; 
 
   public User[] getUsers() {
     // Get data from DB or wherever
     return people;
 }
}