Enterprise Spring boot testing: Part 4 ( Controller, Web layer testing, Integration Testing, MockMvc )
In this part, we will see about the web layer for our web application and how to write the tests for the controllers using MockMvc, and also integration testing without any mocks.
If you are new here, this is a continuation of the multi-part Spring boot testing series. You can see the other links below:
- Enterprise Spring boot testing: Part 1 ( Significance, Types of tests, libraries, project setup )
- Enterprise Spring boot testing: Part 2 ( General concepts, Domain testing, Unit test cases, Repository testing )
- Enterprise Spring boot testing: Part 3 ( Service test cases, Dependency mocking )
- You find the entire project on the Github Repository.
Customer controller 
We will first create a Controller class that will specify the endpoints for making REST API calls for customer service methods. The class will be annotated with @RestController
for informing Spring that it’s a controller class with mapping.
Controller class
package com.microideation.tutorial.springtest.controller; import com.microideation.tutorial.springtest.domain.Customer; import com.microideation.tutorial.springtest.service.CustomerService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.Optional; @RestController @RequestMapping("/customers") public class CustomerController { private final CustomerService customerService; public CustomerController(CustomerService customerService) { this.customerService = customerService; } @PostMapping("/register") public ResponseEntityregisterCustomer(@RequestBody Customer customer) { Customer savedCustomer = customerService.registerCustomer(customer); return ResponseEntity .status(HttpStatus.CREATED) .body(savedCustomer); } @GetMapping("/{custId}") public ResponseEntity getCustomer(@PathVariable Long custId) { Customer customer = customerService.getCustomer(custId); return ResponseEntity .status(HttpStatus.OK) .body(customer); } @PutMapping("/activate") public ResponseEntity activateCustomer(@RequestParam Long custId) { Customer savedCustomer = customerService.activate(custId); return ResponseEntity .status(HttpStatus.OK) .body(savedCustomer); } @PutMapping("/deactivate") public ResponseEntity deactivateCustomer(@RequestParam Long custId) { Customer savedCustomer = customerService.deactivate(custId); return ResponseEntity .status(HttpStatus.OK) .body(savedCustomer); } }
We have 4 APIs for registering, reading customers using id, activating, and deactivating. For each operation, we are calling the respective service methods and returning the HTTP status and the body. As we are using @RestController
, all the methods will be having @RequestBody
and the response will be converted to JSON.
It is not advised to use the Entity directly on the controller ( as in the case of register API ). As a security best practice, we should use a resource class ( POJO or DTO ) and then map to the entity. I am directly using the entity for simplicity and shouldn’t follow it in production grade code.
Controller Advice ( Error handling )
We also create a class annotated with @ControllerAdvice
which will catch any exception that happens in the controller and convert it into a custom response.
package com.microideation.tutorial.springtest.controller; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import java.util.Map; @ControllerAdvice public class ExceptionController { @ExceptionHandler(Exception.class) @ResponseBody public MaphandleException(Exception ex) { return Map.of("status","failed", "error",ex.getMessage()); } }
We have created a handler for the Exception.class
superclass so that all the exceptions are handled and is returning a map with status and message. In actual practice, we can create handlers for each Exception that need to be handled ( IOException, NoSuchElementException, etc ).
Web layer testing ( Controller Test cases and MockMvc )
Now that we have the controller defined, let’s go and create the test cases for testing the controller endpoints.
CustomerControllerTest.java
package com.microideation.tutorial.springtest.customers; import com.fasterxml.jackson.databind.ObjectMapper; import com.microideation.tutorial.springtest.domain.Customer; import com.microideation.tutorial.springtest.service.CustomerService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import java.util.NoSuchElementException; import static com.microideation.tutorial.springtest.customers.CustomerFixture.standardCustomer; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(MockitoExtension.class) @AutoConfigureMockMvc @SpringBootTest @ActiveProfiles("test") public class CustomerControllerTest { @Autowired private MockMvc mockMvc; @MockBean private CustomerService customerService; private ObjectMapper mapper; @BeforeEach public void init() { mapper = new ObjectMapper(); } @Test public void givenGetCustomer_whenIdIsPresent_thenReturnCustomer() throws Exception { // Customer object Customer customer = standardCustomer(); // Mock the get customer when(customerService.getCustomer(eq(customer.getId()))).thenReturn(customer); // Call the service mockMvc.perform(get("/customers/"+customer.getId())) .andExpect(status().isOk()) .andExpect(jsonPath("$.firstname", is(customer.getFirstname()))); // Verify the call to the service verify(customerService,times(1)).getCustomer(eq(customer.getId())); } @Test public void givenGetCustomer_whenIdIsNotFound_thenReturnErrorStatus() throws Exception { // Mock the get customer when(customerService.getCustomer(anyLong())).thenThrow(new NoSuchElementException("No customer")); // Call the service mockMvc.perform(get("/customers/10")) .andExpect(status().isOk()) .andExpect(jsonPath("$.status", is("failed"))); // Verify the call to the service verify(customerService,times(1)).getCustomer(anyLong()); } @Test public void givenRegisterCustomer_whenValidField_thenReturnCustomer() throws Exception { // Customer object Customer customer = standardCustomer(); customer.setId(null); // JSON String cusJson = mapper.writeValueAsString(customer); // Mock the get customer when(customerService.registerCustomer(eq(customer))).thenReturn(customer); // Call the service mockMvc.perform(post("/customers/register") .content(cusJson) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.firstname", is(customer.getFirstname()))); // Verify the call to the service verify(customerService,times(1)).registerCustomer(eq(customer)); } @Test public void givenActivateCustomer_whenValidField_thenReturnCustomerWithActiveStatus() throws Exception { // Customer object Customer customer = standardCustomer(); // JSON String cusJson = mapper.writeValueAsString(customer); // Mock the get customer when(customerService.activate(eq(customer.getId()))).thenReturn(customer); // Call the service mockMvc.perform(put("/customers/activate") .param("custId",customer.getId().toString())) .andExpect(status().isOk()) .andExpect(jsonPath("$.firstname", is(customer.getFirstname()))); // Verify the call to the service verify(customerService,times(1)).activate(eq(customer.getId())); } @Test public void givenDeactivateCustomer_whenValidField_thenReturnCustomerWithDisabledStatus() throws Exception { // Customer object Customer customer = standardCustomer(); // JSON String cusJson = mapper.writeValueAsString(customer); // Mock the get customer when(customerService.deactivate(eq(customer.getId()))).thenReturn(customer); // Call the service mockMvc.perform(put("/customers/deactivate") .param("custId",customer.getId().toString())) .andExpect(status().isOk()) .andExpect(jsonPath("$.firstname", is(customer.getFirstname()))); // Verify the call to the service verify(customerService,times(1)).deactivate(eq(customer.getId())); } }
You can notice that the test class structure and the annotations used are slightly different for the Controller ( web layer ) testing. In the controller test also, we will be mocking the other layers ( service in this case ) so that we can focus only on the Web layer.
We are using the following annotations:
@ExtendWith(MockitoExtension.class)
: This is similar to what we have seen on the service class where we are requesting the Mockito to consider the mocks.@AutoConfigureMvc
: This will tell spring to configure the
MockMvc
dependency for us so that we can autowire it. You may skip and create theMockMvc
manually usingMockMvcBuilders
.@SpringBootTest
: This will start the spring context loader and up the api mappings that we have defined in the
CustomerController
@ActiveProfiles("test")
: We are specifying the profile to be used for running the tests. You may skip if this if your default profile is “test”.
With the above annotations, we are going to autowire the MockMvc that provides the support for calling the API endpoints and options for validation of response. Also, CustomerService
is injected as a @MockBean
so that we can mock the methods.
MockMvc 
MockMvc is a utility for testing the Spring MVC. It provides methods for invoking the endpoints as well as validating the response using a variety of utility methods. The MockMvcRequestBuilders is used for building different HTTP method calls ( post, put, get, etc.) and the MockMvcResultMatchers
are used for the validation of the responses.
Testing Controller
We start the controller test case in the same way we have written the other test cases.
GET API testing
For the first case givenGetCustomer_whenIdIsPresent_thenReturnCustomer()
, we want to make sure that the GET controller is returning the customer with the specified id and HTTP status code as 200 ( OK ). For this, we will mock the CustomerService
to return an object and then make the call to the API using MockMvc
.
// Call the service mockMvc.perform(get("/customers/"+customer.getId())) .andExpect(status().isOk()) .andExpect(jsonPath("$.firstname", is(customer.getFirstname())));
Notice that we are calling the get() method of MockMvcRequestBuilders
passing the URL ( relative ) and validating the status and data
The .andExpect() allows specifying result matchers that we can use for validating the response received. Also, the jsonPath provides an option to refer to the response JSON using a path. Here, $.firstname represents the <response json>.firstname.
Note that the get(), status(), jsonPath() , is() are all static methods on their respective classes and are used directly as they are already imported using static import. You may check that in the import statements.
Similarly, we can write the test case for givenGetCustomer_whenIdIsNotFound_thenReturnErrorStatus()
. The difference here is that we will set the mock to throw an exception and validate that the response has a status field set as “failed” by our @ControllerAdvice
POST API Testing
For testing the POST API ( register API ), we will be using the post() method of MockMvc. Notice the givenRegisterCustomer_whenValidField_thenReturnCustomer
, we are creating a customer object and converting it to JSON. This will be passed to the MockMvc post method as :
mockMvc.perform(post("/customers/register") .content(cusJson) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.firstname", is(customer.getFirstname())));
We pass the JSON payload as content and also set the content type while calling the MockMvc
post() method. The response validation is similar to our previous cases using the status and jsonPath()
PUT API Testing
The PUT API testing uses the put() method and we can pass the parameter using .param() method. The rest of the validations remain the same.
mockMvc.perform(put("/customers/activate") .param("custId",customer.getId().toString())) .andExpect(status().isOk()) .andExpect(jsonPath("$.firstname", is(customer.getFirstname())));
Integration Testing
Integration testing of an application does not include any mocks and uses the actual layers of dependencies. The integration testing in Spring boot is almost similar to the Controller testing, but we won’t be using any mocks for any of the layers. So this requires all the layers ( JPA, Web ) to be available and is advised to be run on a different profile if possible.
package com.microideation.tutorial.springtest.customers; import com.fasterxml.jackson.databind.ObjectMapper; import com.microideation.tutorial.springtest.dictionary.CustomerStatus; import com.microideation.tutorial.springtest.domain.Customer; import com.microideation.tutorial.springtest.service.CustomerService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import java.util.NoSuchElementException; import static com.microideation.tutorial.springtest.customers.CustomerFixture.standardCustomer; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(MockitoExtension.class) @AutoConfigureMockMvc @SpringBootTest @ActiveProfiles("test") public class CustomerIntegrationTest { @Autowired private MockMvc mockMvc; @Autowired private CustomerService customerService; @Autowired private ObjectMapper mapper; @Test public void givenGetCustomer_whenIdIsPresent_thenReturnCustomer() throws Exception { // Customer object Customer customer = standardCustomer(); customer.setId(null); // Save customer = customerService.registerCustomer(customer); // Call the service mockMvc.perform(get("/customers/"+customer.getId())) .andExpect(status().isOk()) .andExpect(jsonPath("$.firstname", is(customer.getFirstname()))); } @Test public void givenGetCustomer_whenIdIsNotFound_thenReturnErrorStatus() throws Exception { // Call the service mockMvc.perform(get("/customers/10000")) .andExpect(status().isOk()) .andExpect(jsonPath("$.status", is("failed"))); } @Test public void givenRegisterCustomer_whenValidField_thenReturnCustomer() throws Exception { // Customer object Customer customer = standardCustomer(); customer.setId(null); // JSON String cusJson = mapper.writeValueAsString(customer); // Call the service mockMvc.perform(post("/customers/register") .content(cusJson) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.firstname", is(customer.getFirstname()))); } @Test public void givenActivateCustomer_whenValidField_thenReturnCustomerWithActiveStatus() throws Exception { // Customer object Customer customer = standardCustomer(); customer.setId(null); // Save customer = customerService.registerCustomer(customer); // Call the service mockMvc.perform(put("/customers/activate") .param("custId",customer.getId().toString())) .andExpect(status().isOk()) .andExpect(jsonPath("$.status", is(CustomerStatus.ACTIVE.name()))) .andExpect(jsonPath("$.firstname", is(customer.getFirstname()))); } @Test public void givenDeactivateCustomer_whenValidField_thenReturnCustomerWithDisabledStatus() throws Exception { // Customer object Customer customer = standardCustomer(); customer.setStatus(CustomerStatus.ACTIVE); customer.setId(null); // Save customer = customerService.registerCustomer(customer); // Call the service mockMvc.perform(put("/customers/deactivate") .param("custId",customer.getId().toString())) .andExpect(status().isOk()) .andExpect(jsonPath("$.status", is(CustomerStatus.DISABLED.name()))) .andExpect(jsonPath("$.firstname", is(customer.getFirstname()))); } }
Following are the points for integration testing
- The annotations used are the same we used for controller test.
- Instead of MockBean, we autowire the actual dependencies.
- For data preparation, we will use the actual service methods for creating data we require for testing.
We usually initiate the integration testing by invoking the REST endpoints as that will trigger the subsequent layers ( API -> Service -> Repository -> Domain ). Hence all our integration tests are using MockMvc for triggering the REST APIs and the validations are done using the same MockMvcResultMatchers
.
Conclusion
In this tutorial series, you have seen how to create a web application using Spring with multiple layers and how to write the test cases for each layer using the standard testing libraries provided by the spring-boot-starter-test
dependencies.
The entire code is available on the Github Repository. But it would be better if you create the project from scratch by yourself and use the code as a reference point. Also, try to add more cases and functionality to cover more ground.
Let me know if you have any comments or queries.
1 Response
[…] In the next post, we will be covering web layer testing ( MockMVC, result validation ), etc. Enterprise Spring boot testing: Part 4 ( Controller, Web layer testing, Integration Testing, MockMvc… […]