Introduction

During this exercise we will add some MockMvc tests to test your freshly created controller(s)

Background info

MockMvc is a framework which enables you to test your controller physically and in isolation without using your Postman tool or such. And it allows you to repeat this test during every Maven build.

There are two main approaches to set up MockMvc:

  • @WebMvcTest (slice test) - loads only the web layer for a specific controller. This is the recommended approach for unit testing controllers in isolation. It is fast because it does not load the full application context.

  • @SpringBootTest + @AutoConfigureMockMvc - loads the full application context. Use this for integration tests where you want to test the full stack.

We will use the @WebMvcTest approach in this exercise.

Since Spring Boot 3.4+, @MockitoBean replaces the older @MockBean annotation. In Spring Boot 4, @MockBean is removed entirely. Use @MockitoBean instead.
Spring Boot 4 also introduces RestTestClient as a unified fluent API for both MockMvc-backed and live-server tests. For this exercise we stick with MockMvc, but be aware that RestTestClient is the modern alternative.

Code

This exercise is pretty code-based, hence below you will find some code for a Car oriented MockMvc controller test.

Be aware of the comments in the code. They are added to prevent you from (accidentally) removing the static imports and then not knowing anymore which imports they were.

Example code for a CarController POST test

package com.acme.carapp.api;

import com.acme.carapp.model.Car;
import com.acme.carapp.service.CarService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.core.Is.is;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/*
 * Static imports reminder (in case your IDE removes them):
 *
 * import static org.hamcrest.core.Is.is;
 * import static org.mockito.Mockito.any;
 * import static org.mockito.Mockito.when;
 * import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 * import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
 * import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 * import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 */

@WebMvcTest(CarController.class) (1)
public class CarControllerTest {

    @Autowired
    private MockMvc mockMvc; (2)

    @MockitoBean (3)
    private CarService carService;

    @Test
    void addCarTest() throws Exception {
        Car car = new Car();
        car.setBrand("Volvo");
        car.setLicensePlate("AB-123-CD");
        car.setMileage(45000);

        Car savedCar = new Car();
        savedCar.setBrand("Volvo");
        savedCar.setLicensePlate("AB-123-CD");
        savedCar.setMileage(45000);
        // Simulate that the DB assigned an id
        // (use reflection or a test constructor to set the id)

        ObjectMapper mapper = new ObjectMapper();
        String json = mapper.writeValueAsString(car);

        when(this.carService.save(any(Car.class))).thenReturn(savedCar);

        this.mockMvc.perform(post("/api/cars")
                .contentType(MediaType.APPLICATION_JSON)
                .content(json))
                .andDo(print())
                .andExpect(jsonPath("$.brand", is(car.getBrand())))
                .andExpect(jsonPath("$.licensePlate", is(car.getLicensePlate())))
                .andExpect(jsonPath("$.mileage", is(car.getMileage())))
                .andExpect(status().isOk());
    }
}
1 @WebMvcTest loads only the web layer for the specified controller - this is a slice test
2 MockMvc is auto-configured and injected by Spring
3 @MockitoBean replaces the old @MockBean (removed in Spring Boot 4)

Example code for CarController GET test

@Test
void findCarByIdTest() throws Exception {
    Car car = new Car();
    car.setBrand("BMW");
    car.setLicensePlate("XY-456-ZZ");
    car.setMileage(12000);

    when(this.carService.findById(3L)).thenReturn(Optional.of(car));

    this.mockMvc.perform(get("/api/cars/3")
            .contentType(MediaType.APPLICATION_JSON))
            .andDo(print())
            .andExpect(jsonPath("$.brand", is(car.getBrand())))
            .andExpect(jsonPath("$.licensePlate", is(car.getLicensePlate())))
            .andExpect(jsonPath("$.mileage", is(car.getMileage())))
            .andExpect(status().isOk());
}

Example code for CarController GET (not found) test

@Test
void findCarByIdNotFoundTest() throws Exception {

    when(this.carService.findById(999L)).thenReturn(Optional.empty());

    this.mockMvc.perform(get("/api/cars/999")
            .contentType(MediaType.APPLICATION_JSON))
            .andDo(print())
            .andExpect(status().isNotFound());
}

Example code for CarController DELETE test

@Test
void deleteCarByIdTest() throws Exception {
    Car car = new Car();
    car.setBrand("Audi");

    when(this.carService.findById(1L)).thenReturn(Optional.of(car));

    this.mockMvc.perform(delete("/api/cars/1")
            .contentType(MediaType.APPLICATION_JSON))
            .andDo(print())
            .andExpect(status().isNoContent());
}

Exercise

The exercise consists of moving the code above to your frame of reference, i.e. your CarApp (or your personal hobby project).

Steps
  • Create a test class for your CarController (or your own controller)

  • Add tests for the POST, GET, PUT and DELETE endpoints

  • Run the tests using mvn clean test or from your IDE

Tips
  • Make sure to use @WebMvcTest(YourController.class) to keep the test fast

  • Use @MockitoBean to mock the service layer

  • Use the ObjectMapper to convert your objects to JSON for POST and PUT requests

  • Use jsonPath expressions to validate the response body

  • Use status().isOk(), status().isCreated(), status().isNoContent(), status().isNotFound() to validate the HTTP status codes

Further reading