Unit Test Spring MVC Rest Service: MockMVC, JUnit, Mockito
Previously we saw how to create a spring mvc restful web service. In this tutorial we continue by showing how to unit test this spring mvc rest service using JUnit, Mockito and Spring Test (MockMVC).
Unit testing is a software testing method to test individual units of source code. Each unit test can contain different mocked data to validate different scenario’s. Mockito is a great mocking framework which we’ll be using to provide data for our JUnit tests. JUnit is by far the most popular unit test framework.
We start by showing how to configure Mockito, to mock the responses returned from the service, and inject the mocks into the rest service controller. Next, we’ll explain each unit test individually. Then, we’ll show you an overview of the entire test class. You can download the full working example on the bottom of the page, this includes the entire rest service together with all the services and unit tests.
Maven Dependencies
Add the following dependencies to your project’s pom.xml and maven will resolve the dependencies automatically.
- org.hamcrest:hamcrest We use hamcrest for writing assertions on the response. We can use a variety of Matchers to validate if the response is what we expect.
- org.springframework:spring-test contains MockMvc and other test classes which we can use to perform and validate requests on a specific endpoint.
- org.mockito:mockito-core mocking framework for mocking data.
- com.jayway.jsonpath:json-path-assert Using jsonPath() we can access the response body assertions, to inspect a specific subset of the body. We can use hamcrest Matchers for asserting the value found at the JSON path.
<?xml version="1.0" encoding="UTF-8"?> <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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.memorynotfound.spring.mvc.rest</groupId> <artifactId>rest-template-integration-test</artifactId> <version>1.0.0-SNAPSHOT</version> <name>SPRING-MVC - ${project.artifactId}</name> <url>https://memorynotfound.com</url> <packaging>war</packaging> <properties> <spring.version>4.3.1.RELEASE</spring.version> <jackson.version>2.7.5</jackson.version> <logback.version>1.1.7</logback.version> </properties> <dependencies> <!-- spring libraries --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <!-- Needed for JSON View --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <!-- logging --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> <!-- servlet api --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <!-- testing --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> <exclusions> <exclusion> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-core</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-all</artifactId> <version>1.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>1.10.19</version> <exclusions> <exclusion> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-core</artifactId> </exclusion> </exclusions> <scope>test</scope> </dependency> <dependency> <groupId>com.jayway.jsonpath</groupId> <artifactId>json-path-assert</artifactId> <version>2.2.0</version> <scope>test</scope> <exclusions> <exclusion> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-core</artifactId> </exclusion> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.2</version> <configuration> <source>1.7</source> <target>1.7</target> </configuration> </plugin> <plugin> <artifactId>maven-war-plugin</artifactId> <version>2.6</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> </plugins> </build> </project>
Summary of Previous Tutorial
Previously we saw how to create a spring mvc restful web service. Some important classes explained:
- UserController contains the rest endpoints for every CRUD operation matching the GET, PUT, POST and delete HTTP request methods.
- UserService handles all the CRUD operations managed by an in-memory list.
- CORSFilter adds cross-origin resource sharing headers to every request.
Configuring Mockito and MockMvc
By annotating the UserService with the @Mock annotation, we can return mocked data when we call a method from this service. Using the @InjectMocks annotation, we can inject the mocked service inside our UserController. Before each test, we must initialize these mocks using the MockitoAnnotations#initMocks(this).
The MockMvc is initialized using the MockMvcBuilders#standaloneSetup(...).build() method. Optionally we can add filters, interceptors or etc. using the .addFilter() or .addInterceptor() methods.
public class UserControllerUnitTest { private MockMvc mockMvc; @Mock private UserService userService; @InjectMocks private UserController userController; @Before public void init(){ MockitoAnnotations.initMocks(this); mockMvc = MockMvcBuilders .standaloneSetup(userController) .addFilters(new CORSFilter()) .build(); } // ... }
- MockMvc is the main entry point for server-side Spring MVC test support. Perform a request and return a type that allows chaining further actions, such as asserting expectations, on the result.
- @Mock creating a mock. This can also be achieved by using org.mockito.mock(..) method.
- @InjectMocks injects mock or spy fields into tested objects automatically.
- MockitoAnnotations.initMocks(this) initializes fields annotated with Mockito annotations.
- MockMvcBuilders.standaloneSetup(..).build() builds a MockMvc instance by registering one or more @Controller instances and configuring Spring MVC infrastructure programmatically.
Overview Unit Tests
Now that we have configured Mockito with Spring Test Framework, we can start writing our unit tests for our spring mvc rest service. The endpoints in the rest service represent common CRUD operations like GET, POST, PUT and DELETE, as such we are going to unit test each operation for successes and failures. Here is an overview of each HTTP method:
- Unit Test HTTP GET getting all users.
- Unit Test HTTP GET/PathVariable get a user by id.
- Unit Test HTTP POST create a new user.
- Unit Test HTTP PUT update an existing user.
- Unit Test HTTP DELETE delete a user by id.
- Unit Test HTTP HEADERS verify if headers are correctly set.
HTTP GET Unit Test: get all
GET /users
- Create test data which’ll be returned as a response in the rest service.
- Configure mock object to return the test data when the getAll() method of the UserService is invoked.
- Invoke an HTTP GET request to the /users URI.
- Validate if the response is correct.
- Verify that the HTTP status code is 200 (OK).
- Verify that the content-type of the response is application/json and its character set is UTF-8.
- Verify that the collection contains 2 items.
- Verify that the id attribute of the first element equals to 1.
- Verify that the username attribute of the first element equals to Daenerys Targaryen.
- Verify that the getAll() method of the UserService is invoked exactly once.
- Verify that after the response, no more interactions are made to the UserService
@Test public void test_get_all_success() throws Exception { List<User> users = Arrays.asList( new User(1, "Daenerys Targaryen"), new User(2, "John Snow")); when(userService.getAll()).thenReturn(users); mockMvc.perform(get("/users")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) .andExpect(jsonPath("$", hasSize(2))) .andExpect(jsonPath("$[0].id", is(1))) .andExpect(jsonPath("$[0].username", is("Daenerys Targaryen"))) .andExpect(jsonPath("$[1].id", is(2))) .andExpect(jsonPath("$[1].username", is("John Snow"))); verify(userService, times(1)).getAll(); verifyNoMoreInteractions(userService); }
@Test public void test_create_user_fail_404_not_found() throws Exception { User user = new User("username exists"); when(userService.exists(user)).thenReturn(true); mockMvc.perform( post("/users") .contentType(MediaType.APPLICATION_JSON) .content(asJsonString(user))) .andExpect(status().isConflict()); verify(userService, times(1)).exists(user); verifyNoMoreInteractions(userService); }
HTTP GET Unit Test with PathVariable: get by id
GET /users/1
- Create test data and configure mock object to return the data when the findById() method of the UserService is invoked.
- Invoke an HTTP GET request to the /users/1 URI.
- Validate if the response is correct.
- Verify that the HTTP status code is 200 (OK).
- Verify that the content-type of the response is application/json and its character set is UTF-8.
- Verify that the id and username attributes are equal to the mocked test data.
- Verify that the findById() method of the UserService is invoked exactly once.
- Verify that after the response, no more interactions are made to the UserService
@Test public void test_get_by_id_success() throws Exception { User user = new User(1, "Daenerys Targaryen"); when(userService.findById(1)).thenReturn(user); mockMvc.perform(get("/users/{id}", 1)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) .andExpect(jsonPath("$.id", is(1))) .andExpect(jsonPath("$.username", is("Daenerys Targaryen"))); verify(userService, times(1)).findById(1); verifyNoMoreInteractions(userService); }
- Configure mock object to return null when the findById() method of the UserService is invoked.
- Invoke an HTTP GET request to the /users/1 URI.
- Validate if the response is correct.
- Verify that the HTTP status code is 404 (Not Found).
- Verify that the findById() method of the UserService is invoked exactly once.
- Verify that after the response, no more interactions are made to the UserService
@Test public void test_get_by_id_fail_404_not_found() throws Exception { when(userService.findById(1)).thenReturn(null); mockMvc.perform(get("/users/{id}", 1)) .andExpect(status().isNotFound()); verify(userService, times(1)).findById(1); verifyNoMoreInteractions(userService); }
HTTP POST Unit Test: create
POST /users
- Configure mocked responses for the UserService exists() and create methods.
- Invoke an HTTP POST request to the /users URI. Make sure the content-type is set to application/json. Convert the User object to JSON and add it to the request.
- Validate if the response is correct.
- Verify that the HTTP status code is 201 (CREATED).
- Verify that the location header is set with the path to the created resource.
- Verify that the exists() and create() methods of the UserService are invoked exactly once.
- Verify that after the response, no more interactions are made to the UserService
@Test public void test_create_user_success() throws Exception { User user = new User("Arya Stark"); when(userService.exists(user)).thenReturn(false); doNothing().when(userService).create(user); mockMvc.perform( post("/users") .contentType(MediaType.APPLICATION_JSON) .content(asJsonString(user))) .andExpect(status().isCreated()) .andExpect(header().string("location", containsString("http://localhost/users/"))); verify(userService, times(1)).exists(user); verify(userService, times(1)).create(user); verifyNoMoreInteractions(userService); }
HTTP PUT Unit Test: update
PUT /users/1
- Configure mocked responses for the UserService findById() and update methods.
- Invoke an HTTP PUT request to the /users/1 URI. Make sure the content-type is set to application/json. Convert the User object to JSON and add it to the request.
- Validate if the response is correct.
- Verify that the HTTP status code is 200 (OK).
- Verify that the findById() and update() methods of the UserService are invoked exactly once.
- Verify that after the response, no more interactions are made to the UserService
@Test public void test_update_user_success() throws Exception { User user = new User(1, "Arya Stark"); when(userService.findById(user.getId())).thenReturn(user); doNothing().when(userService).update(user); mockMvc.perform( put("/users/{id}", user.getId()) .contentType(MediaType.APPLICATION_JSON) .content(asJsonString(user))) .andExpect(status().isOk()); verify(userService, times(1)).findById(user.getId()); verify(userService, times(1)).update(user); verifyNoMoreInteractions(userService); }
HTTP DELETE Unit Test: delete
DELETE /users/1
- Configure mocked responses for the UserService findById() and delete methods.
- Invoke an HTTP DELETE request to the /users/1 URI. Make sure the content-type is set to application/json. Convert the User object to JSON and add it to the request.
- Validate if the response is correct.
- Verify that the HTTP status code is 200 (OK).
- Verify that the findById() and delete() methods of the UserService are invoked exactly once.
- Verify that after the response, no more interactions are made to the UserService
@Test public void test_delete_user_success() throws Exception { User user = new User(1, "Arya Stark"); when(userService.findById(user.getId())).thenReturn(user); doNothing().when(userService).delete(user.getId()); mockMvc.perform( delete("/users/{id}", user.getId())) .andExpect(status().isOk()); verify(userService, times(1)).findById(user.getId()); verify(userService, times(1)).delete(user.getId()); verifyNoMoreInteractions(userService); }
HTTP HEADERS Unit Test
GET /users
- Invoke an HTTP GET request to the /users URI.
- Validate if the correct headers are set.
- Access-Control-Allow-Origin should be equal to *.
- Access-Control-Allow-Methods should be equal to POST, GET, PUT, OPTIONS, DELETE.
- Access-Control-Allow-Headers should be equal to *.
- Access-Control-Max-Age should be equal to 3600.
@Test public void test_cors_headers() throws Exception { mockMvc.perform(get("/users")) .andExpect(header().string("Access-Control-Allow-Origin", "*")) .andExpect(header().string("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE")) .andExpect(header().string("Access-Control-Allow-Headers", "*")) .andExpect(header().string("Access-Control-Max-Age", "3600")); }
Convert Java Object to JSON
This piece of code is used to write an object into JSON representation.
public static String asJsonString(final Object obj) { try { return new ObjectMapper().writeValueAsString(obj); } catch (Exception e) { throw new RuntimeException(e); } }
Full Example
Here is an overview of the full working example. You can download the entire project at the bottom of the page.
package com.memorynotfound.test; import com.fasterxml.jackson.databind.ObjectMapper; import com.memorynotfound.controller.UserController; import com.memorynotfound.filter.CORSFilter; import com.memorynotfound.model.User; import com.memorynotfound.service.UserService; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.http.*; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import java.util.Arrays; import java.util.List; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.core.Is.is; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; public class UserControllerUnitTest { private static final int UNKNOWN_ID = Integer.MAX_VALUE; private MockMvc mockMvc; @Mock private UserService userService; @InjectMocks private UserController userController; @Before public void init(){ MockitoAnnotations.initMocks(this); mockMvc = MockMvcBuilders .standaloneSetup(userController) .addFilters(new CORSFilter()) .build(); } // =========================================== Get All Users ========================================== @Test public void test_get_all_success() throws Exception { List<User> users = Arrays.asList( new User(1, "Daenerys Targaryen"), new User(2, "John Snow")); when(userService.getAll()).thenReturn(users); mockMvc.perform(get("/users")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) .andExpect(jsonPath("$", hasSize(2))) .andExpect(jsonPath("$[0].id", is(1))) .andExpect(jsonPath("$[0].username", is("Daenerys Targaryen"))) .andExpect(jsonPath("$[1].id", is(2))) .andExpect(jsonPath("$[1].username", is("John Snow"))); verify(userService, times(1)).getAll(); verifyNoMoreInteractions(userService); } // =========================================== Get User By ID ========================================= @Test public void test_get_by_id_success() throws Exception { User user = new User(1, "Daenerys Targaryen"); when(userService.findById(1)).thenReturn(user); mockMvc.perform(get("/users/{id}", 1)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) .andExpect(jsonPath("$.id", is(1))) .andExpect(jsonPath("$.username", is("Daenerys Targaryen"))); verify(userService, times(1)).findById(1); verifyNoMoreInteractions(userService); } @Test public void test_get_by_id_fail_404_not_found() throws Exception { when(userService.findById(1)).thenReturn(null); mockMvc.perform(get("/users/{id}", 1)) .andExpect(status().isNotFound()); verify(userService, times(1)).findById(1); verifyNoMoreInteractions(userService); } // =========================================== Create New User ======================================== @Test public void test_create_user_success() throws Exception { User user = new User("Arya Stark"); when(userService.exists(user)).thenReturn(false); doNothing().when(userService).create(user); mockMvc.perform( post("/users") .contentType(MediaType.APPLICATION_JSON) .content(asJsonString(user))) .andExpect(status().isCreated()) .andExpect(header().string("location", containsString("http://localhost/users/"))); verify(userService, times(1)).exists(user); verify(userService, times(1)).create(user); verifyNoMoreInteractions(userService); } @Test public void test_create_user_fail_409_conflict() throws Exception { User user = new User("username exists"); when(userService.exists(user)).thenReturn(true); mockMvc.perform( post("/users") .contentType(MediaType.APPLICATION_JSON) .content(asJsonString(user))) .andExpect(status().isConflict()); verify(userService, times(1)).exists(user); verifyNoMoreInteractions(userService); } // =========================================== Update Existing User =================================== @Test public void test_update_user_success() throws Exception { User user = new User(1, "Arya Stark"); when(userService.findById(user.getId())).thenReturn(user); doNothing().when(userService).update(user); mockMvc.perform( put("/users/{id}", user.getId()) .contentType(MediaType.APPLICATION_JSON) .content(asJsonString(user))) .andExpect(status().isOk()); verify(userService, times(1)).findById(user.getId()); verify(userService, times(1)).update(user); verifyNoMoreInteractions(userService); } @Test public void test_update_user_fail_404_not_found() throws Exception { User user = new User(UNKNOWN_ID, "user not found"); when(userService.findById(user.getId())).thenReturn(null); mockMvc.perform( put("/users/{id}", user.getId()) .contentType(MediaType.APPLICATION_JSON) .content(asJsonString(user))) .andExpect(status().isNotFound()); verify(userService, times(1)).findById(user.getId()); verifyNoMoreInteractions(userService); } // =========================================== Delete User ============================================ @Test public void test_delete_user_success() throws Exception { User user = new User(1, "Arya Stark"); when(userService.findById(user.getId())).thenReturn(user); doNothing().when(userService).delete(user.getId()); mockMvc.perform( delete("/users/{id}", user.getId())) .andExpect(status().isOk()); verify(userService, times(1)).findById(user.getId()); verify(userService, times(1)).delete(user.getId()); verifyNoMoreInteractions(userService); } @Test public void test_delete_user_fail_404_not_found() throws Exception { User user = new User(UNKNOWN_ID, "user not found"); when(userService.findById(user.getId())).thenReturn(null); mockMvc.perform( delete("/users/{id}", user.getId())) .andExpect(status().isNotFound()); verify(userService, times(1)).findById(user.getId()); verifyNoMoreInteractions(userService); } // =========================================== CORS Headers =========================================== @Test public void test_cors_headers() throws Exception { mockMvc.perform(get("/users")) .andExpect(header().string("Access-Control-Allow-Origin", "*")) .andExpect(header().string("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE")) .andExpect(header().string("Access-Control-Allow-Headers", "*")) .andExpect(header().string("Access-Control-Max-Age", "3600")); } /* * converts a Java object into JSON representation */ public static String asJsonString(final Object obj) { try { return new ObjectMapper().writeValueAsString(obj); } catch (Exception e) { throw new RuntimeException(e); } } }
Download
From:一号门
COMMENTS