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:

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 ResponseEntity registerCustomer(@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 Map handleException(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:

  1. @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.
  2. @AutoConfigureMvc : This will tell spring to configure the MockMvc dependency for us so that we can autowire it. You may skip and create the MockMvc manually using MockMvcBuilders.
  3. @SpringBootTest : This will start the spring context loader and up the api mappings that we have defined in the CustomerController
  4. @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

  1. The annotations used are the same we used for controller test.
  2. Instead of MockBean, we autowire the actual dependencies.
  3. 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.

You may also like...

1 Response

  1. November 11, 2021

    […] 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… […]

Leave a Reply

Your email address will not be published. Required fields are marked *