Unit Tests and Integration Tests
@SpringBootTest annotation will load the fully ApplicationContext. Therefore it is highly used for writing the integration testing in web server environment. This will not use slicing and scan for all the stereotype annotations (@Component, @Service, @Respository and @Controller / @RestController) and loads the full application context. Therefore this is more good at in the context of writing integration testing for the application.
@WebMvcTest annotation will load only the controller layer of the application. This will scan only the @Controller/ @RestController annotation and will not load the fully ApplicationContext. If there is any dependency in the controller layer (has some dependency to other beans from your service layer), you need to provide them manually by mocking those objects.
Therefore @SpringBootTest is widely used for Integration Testing purpose and @WebMvcTest is used for controller layer Unit testing.
@WebMvcTest
As already described above, @WebMvcTest annotation can be used to test the controller slice of the application. This annotation is going to scan only the controllers (including RestControllers) you’ve defined and the MVC infrastructure. That’s it. So if your controller has some dependency to other beans from your service layer, the test won’t start until you either load that config yourself or provide a mock for it. This is much faster as we only load a tiny portion of your app and good for writing the Unit Tests for the Controller layer.
@WebMvcTest will :
- Auto-configure Spring MVC, Jackson, Gson, Message converters etc.
- Load relevant components (@Controller, @RestController, @JsonComponent etc)
- Configure MockMVC
@WebMvcTest is limited (bound) to a single controller and is used in combination with @MockBean to provide mock implementations for required collaborators.
e.g:- @WebMvcTest(UserController.class)
As you can see that above @WebMvcTest annotation is bound to the UserController.
MockMvc
@WebMvcTest also auto-configures MockMvc. MockMVC offers a powerful way to quickly test MVC controllers without needing to start a full HTTP server.
No Web Server is Started with @WebMvcTest
@WebMvcTest does not start any server. Since the web server is not started, RestTemplate or TestRestTemplate cannot be used with @WebMvcTest environment.
If you want to use RestTemplate or TestRestTemplate, then you need to start a server with @SpringBootTest (using webEnviroment attribute).
Integrating Spring Security with MockMvc
Since WebMvcTest is only sliced controller layer for the testing, it would not take the security configurations. Therefore the main Spring Security Configuration class should be imported to your Test Class.
@Import(SpringSecurityConfig.class)
After adding the above import statement for the Test class, the Spring Security context will be accessible for all the test cases under that test class. Therefore the test cases can send authenticated requests to access the secured REST endpoints (that are secured with Spring Security). If you look at the UserControllerWebMvcTest, you can see that main Spring Security Configuration class is imported (please refer the below code segment).
@RunWith(SpringRunner.class) @WebMvcTest(UserController.class) @Import(SpringSecurityConfig.class) public class UserControllerWebMvcTest {
UserControllerWebMvcTest.java
package com.springbootdev.examples.security.basic.controller; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import com.springbootdev.examples.security.basic.config.SpringSecurityConfig; | |
import com.springbootdev.examples.security.basic.model.dto.request.AddUserRequest; | |
import com.springbootdev.examples.security.basic.model.dto.response.AddUserResponse; | |
import com.springbootdev.examples.security.basic.model.dto.response.FindUserResponse; | |
import com.springbootdev.examples.security.basic.service.UserService; | |
import static org.junit.Assert.*; | |
import org.hamcrest.CoreMatchers; | |
import org.junit.Test; | |
import org.junit.runner.RunWith; | |
import org.mockito.ArgumentMatchers; | |
import org.mockito.Mockito; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; | |
import org.springframework.boot.test.mock.mockito.MockBean; | |
import org.springframework.context.annotation.Import; | |
import org.springframework.http.*; | |
import org.springframework.test.context.junit4.SpringRunner; | |
import org.springframework.test.web.servlet.MockMvc; | |
import org.springframework.test.web.servlet.MvcResult; | |
import static org.junit.Assert.assertEquals; | |
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; | |
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; | |
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; | |
@RunWith(SpringRunner.class) | |
@WebMvcTest(UserController.class) | |
@Import(SpringSecurityConfig.class) | |
public class UserControllerWebMvcTest { | |
@Autowired | |
private MockMvc mockMvc; | |
@Autowired | |
private ObjectMapper objectMapper; | |
@MockBean | |
private UserService userService; | |
/* | |
trying to create user with valid credentials and without a request body. | |
Therefore Bad Request error is expected with 400 status code | |
*/ | |
@Test | |
public void testCreateUser1() throws Exception { | |
String username = "chathuranga"; | |
String password = "123"; | |
Integer userId = 1; | |
String name = "Chathuranga T"; | |
String date = "2017-10-10"; | |
//building the mock response | |
AddUserResponse addUserResponse = AddUserResponse.builder() | |
.userId(userId) | |
.username(username) | |
.createdOn(date) | |
.build(); | |
//mocking the bean for any object of AddUserRequest.class | |
Mockito.when(userService.create(ArgumentMatchers.any(AddUserRequest.class))).thenReturn(addUserResponse); | |
//here no request body is added. Therefore the backend server should throw the BadRequest Exception | |
mockMvc.perform(post("/users") | |
.with(httpBasic(username, password))) | |
.andExpect(status().isBadRequest()); | |
} | |
/* | |
trying to create user with valid credentials and proper request body. | |
*/ | |
@Test | |
public void testCreateUser2() throws Exception { | |
String username = "chathuranga"; | |
String password = "123"; | |
Integer userId = 1; | |
String name = "Chathuranga T"; | |
String date = "2017-10-10"; | |
//building the request object | |
AddUserRequest addUserRequest = AddUserRequest.builder() | |
.name(name) | |
.username(username) | |
.password(password) | |
.build(); | |
//building the mock response | |
AddUserResponse addUserResponse = AddUserResponse.builder() | |
.userId(userId) | |
.username(username) | |
.createdOn(date) | |
.build(); | |
//mocking the bean for any object of AddUserRequest.class | |
Mockito.when(userService.create(ArgumentMatchers.any(AddUserRequest.class))).thenReturn(addUserResponse); | |
//response is retrieved as MvcResult | |
MvcResult mvcResult = mockMvc.perform(post("/users") | |
.contentType(MediaType.APPLICATION_JSON) | |
.content(objectMapper.writeValueAsString(addUserRequest)) | |
.with(httpBasic(username, password))) | |
.andExpect(status().isOk()) | |
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) | |
.andExpect(jsonPath("$.user_id", CoreMatchers.is(userId))) | |
.andExpect(jsonPath("$.username", CoreMatchers.is(username))) | |
.andExpect(jsonPath("$.created_on", CoreMatchers.is(date))) | |
.andReturn(); | |
//json response body is converted/mapped to the Java Object | |
String jsonResponse = mvcResult.getResponse().getContentAsString(); | |
AddUserResponse userCreated = new ObjectMapper().readValue(jsonResponse, AddUserResponse.class); | |
assertNotNull(userCreated); | |
assertEquals(userCreated.getUserId(), userId); | |
assertEquals(userCreated.getUsername(), username); | |
assertEquals(userCreated.getCreatedOn(), date); | |
} | |
/* | |
trying to create user with invalid credentials and proper request body. | |
(401 unauthorized) | |
*/ | |
@Test | |
public void testCreateUser3() throws Exception { | |
String username = "invalid_username"; | |
String password = "invalid_password"; | |
Integer userId = 1; | |
String name = "Chathuranga T"; | |
String date = "2017-10-10"; | |
//building the request object | |
AddUserRequest addUserRequest = AddUserRequest.builder() | |
.name(name) | |
.username(username) | |
.password(password) | |
.build(); | |
//building the mock response | |
AddUserResponse addUserResponse = AddUserResponse.builder() | |
.userId(userId) | |
.username(username) | |
.createdOn(date) | |
.build(); | |
//mocking the bean for any object of AddUserRequest.class | |
Mockito.when(userService.create(ArgumentMatchers.any(AddUserRequest.class))).thenReturn(addUserResponse); | |
mockMvc.perform(post("/users") | |
.contentType(MediaType.APPLICATION_JSON) | |
.content(objectMapper.writeValueAsString(addUserRequest)) | |
.with(httpBasic(username, password)) | |
).andExpect(status().isUnauthorized()); | |
} | |
/* | |
trying to create user with valid credentials. but with invalid request body | |
(with empty fields) | |
*/ | |
@Test | |
public void testCreateUser4() throws Exception { | |
String username = "chathuranga"; | |
String password = "123"; | |
Integer userId = 1; | |
String name = "Chathuranga T"; | |
String date = "2017-10-10"; | |
//building the request object (object with empty attributes) | |
AddUserRequest addUserRequest = AddUserRequest.builder().build(); | |
//building the mock response | |
AddUserResponse addUserResponse = AddUserResponse.builder() | |
.userId(userId) | |
.username(username) | |
.createdOn(date) | |
.build(); | |
//mocking the bean for any object of AddUserRequest.class | |
Mockito.when(userService.create(ArgumentMatchers.any(AddUserRequest.class))).thenReturn(addUserResponse); | |
mockMvc.perform(post("/users") | |
.contentType(MediaType.APPLICATION_JSON) | |
.content(objectMapper.writeValueAsString(addUserRequest)) | |
.with(httpBasic(username, password))) | |
.andExpect(status().isUnprocessableEntity()); | |
} | |
/* | |
trying to get the user by providing valid user id | |
*/ | |
@Test | |
public void testFindUserById1() throws Exception { | |
String username = "chathuranga"; | |
String password = "123"; | |
Integer userId = 1; | |
String name = "Chathuranga T"; | |
//building the mock response | |
FindUserResponse findUserResponse = FindUserResponse.builder() | |
.userId(userId) | |
.name(name) | |
.username(username) | |
.build(); | |
//mocking the bean | |
Mockito.when(userService.findUserById(userId)).thenReturn(findUserResponse); | |
//response is retrieved as MvcResult | |
mockMvc.perform(get("/users/{id}", userId) | |
.accept(MediaType.APPLICATION_JSON) | |
.with(httpBasic(username, password))) | |
.andExpect(status().isOk()) | |
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) | |
.andExpect(jsonPath("$.user_id", CoreMatchers.is(userId))) | |
.andExpect(jsonPath("$.name", CoreMatchers.is(name))) | |
.andExpect(jsonPath("$.username", CoreMatchers.is(username))); | |
} | |
/* | |
trying to get the user by providing invalid user Id | |
*/ | |
@Test | |
public void testFindUserById2() throws Exception { | |
String username = "chathuranga"; | |
String password = "123"; | |
Integer userId = –1; | |
String name = "Chathuranga T"; | |
//building the mock response | |
FindUserResponse findUserResponse = FindUserResponse.builder() | |
.userId(userId) | |
.name(name) | |
.username(username) | |
.build(); | |
//mocking the bean | |
Mockito.when(userService.findUserById(userId)).thenReturn(findUserResponse); | |
//response is retrieved as MvcResult | |
mockMvc.perform(get("/users/{id}", userId) | |
.accept(MediaType.APPLICATION_JSON) | |
.with(httpBasic(username, password))) | |
.andExpect(status().isUnprocessableEntity()); | |
} | |
} |
For the demonstration purpose of this article, i will extract few important points from UserControllerWebMvcTest class. If you want to look at the full source code, please download it from GitHub. (Please refer the source code section of this article)
Here are some important things to note ….
Mockito.when(userService.create(ArgumentMatchers.any(AddUserRequest.class))).thenReturn(addUserResponse);
Here the ‘UserService.create’ method is mocked to return ‘addUserResponse‘ object. the method can accept any object which is type is ‘AddUserRequest.class‘.
‘addUserRequest‘ is used as the request body of the POST /users REST Api invocation.
httpBasic(username, password) :- for HTTP basic authentication.
MvcResult will hold the return response after executing the Test case.
String jsonResponse = mvcResult.getResponse().getContentAsString(); AddUserResponse userCreated = new ObjectMapper().readValue(jsonResponse, AddUserResponse.class);
Converting the response body stored in the MvcResult object into the type of AddUserResponse.class.
Source Code
The full source code of this article can be found at GitHub. Click here to download it. For displaying the code coverage analysis, i have used the JaCoco with SonarQube.
Code Coverage Analysis with JaCoco and SonarQube
You can run the test cases and generate the JaCoco code coverage analysis report with SonarQube with following command.
mvn test sonar:sonar -Dsonar.login=admin -Dsonar.password=admin
(Please change the login credentials according to your SonarQube server)
After executing the above command successfully, you can access the code coverage analysis report with your web browser. (For the URL pattern, please refer the article about JaCoCo and SonarQube). It should show you the following report.