Spring Boot + WebMvcTest: Testing REST Controllers

Introduction

@WebMvcTest is a specialized test annotation in Spring Boot used to test REST controllers in isolation. It initializes only the Spring MVC components required for testing controllers, without loading the full application context. This makes tests faster and more focused on the web layer.

In this guide, we will cover how to test a REST controller using @WebMvcTest, MockMvc, and MockitoBean.

1 Understanding WebMvcTest Scope

When using @WebMvcTest, Spring Boot scans and loads only specific beans related to the web layer. The following beans will be included:

  • @Controller
  • @ControllerAdvice
  • @JsonComponent
  • Converter / GenericConverter
  • Filter
  • WebMvcConfigurer
  • HandlerMethodArgumentResolver

Regular @Component, @Service, or @Repository beans are not scanned. This means that services and repositories need to be explicitly mocked to avoid missing dependencies in the test context.

2 Project Setup

To begin, set up a Spring Boot project with the required dependencies. Ensure your pom.xml includes the following dependencies:

  • Spring Boot Starter Web (for building REST APIs)
  • Spring Boot Starter Test (for testing)
  • Mockito (for mocking dependencies)

Your pom.xml should contain the necessary dependencies as shown in [pom.xml].

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>tech.devblueprint</groupId>
    <artifactId>webcontroller-spring-boot-testing-example</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>webcontroller-spring-boot-testing-example</name>
    <description>Demo project for Spring Boot web controller testing</description>
    <url/>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

 

Next, create the following components:

Entity Layer: A Device entity representing a database table [Device.java].

package tech.devblueprint.webcontroller_spring_boot_testing_example.entity;

import jakarta.persistence.*;


@Entity
@Table(name = "devices")
public class Device {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    public Device() {
    }

    public Device(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

 

DTO Layer: A DeviceDto class used for transferring data [DeviceDto.java].

package tech.devblueprint.webcontroller_spring_boot_testing_example.dto;

public class DeviceDto {

    private Long id;
    private String name;

    public DeviceDto() {
    }

    public DeviceDto(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

 

Repository Layer: A DeviceRepository extending JpaRepository for database interactions [DeviceRepository.java].

package tech.devblueprint.webcontroller_spring_boot_testing_example.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import tech.devblueprint.webcontroller_spring_boot_testing_example.entity.Device;

public interface DeviceRepository extends JpaRepository<Device, Long> {
}

 

Service Layer: A DeviceService to handle business logic [DeviceService.java].

package tech.devblueprint.webcontroller_spring_boot_testing_example.service;

import org.springframework.stereotype.Service;
import tech.devblueprint.webcontroller_spring_boot_testing_example.dto.DeviceDto;
import tech.devblueprint.webcontroller_spring_boot_testing_example.entity.Device;
import tech.devblueprint.webcontroller_spring_boot_testing_example.repository.DeviceRepository;

import java.util.List;
import java.util.stream.Collectors;

@Service
public class DeviceService {

    private final DeviceRepository deviceRepository;

    public DeviceService(DeviceRepository deviceRepository) {
        this.deviceRepository = deviceRepository;
    }

    public List<DeviceDto> getAllDevices() {
        List<Device> devices = deviceRepository.findAll();
        return devices.stream()
                .map(device -> new DeviceDto(device.getId(), device.getName()))
                .toList();
    }
}

 

Controller Layer: A DeviceController to expose REST endpoints [DeviceController.java].

package tech.devblueprint.webcontroller_spring_boot_testing_example.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.devblueprint.webcontroller_spring_boot_testing_example.dto.DeviceDto;
import tech.devblueprint.webcontroller_spring_boot_testing_example.service.DeviceService;

import java.util.List;

@RestController
@RequestMapping("/devices")
public class DeviceController {

    private final DeviceService deviceService;

    public DeviceController(DeviceService deviceService) {
        this.deviceService = deviceService;
    }

    @GetMapping
    public List<DeviceDto> getDevices() {
        return deviceService.getAllDevices();
    }
}

[GlobalExceptionHandler.java]

package tech.devblueprint.webcontroller_spring_boot_testing_example.controller.advice;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleAllExceptions(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("An error occurred, and handled: " + ex.getMessage());
    }
}

 

3.Writing Tests with WebMvcTest

The test for our REST controller is implemented in [DeviceControllerTest.java].

package tech.devblueprint.webcontroller_spring_boot_testing_example.controller;

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.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import tech.devblueprint.webcontroller_spring_boot_testing_example.dto.DeviceDto;
import tech.devblueprint.webcontroller_spring_boot_testing_example.service.DeviceService;

import java.util.Arrays;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(DeviceController.class)
public class DeviceControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    private DeviceService deviceService;

    @Test
    public void testGetDevices() throws Exception {
        DeviceDto device1 = new DeviceDto(1L, "Device 1");
        DeviceDto device2 = new DeviceDto(2L, "Device 2");
        when(deviceService.getAllDevices()).thenReturn(Arrays.asList(device1, device2));

        mockMvc.perform(get("/devices")
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].id").value(1))
                .andExpect(jsonPath("$[0].name").value("Device 1"))
                .andExpect(jsonPath("$[1].id").value(2))
                .andExpect(jsonPath("$[1].name").value("Device 2"));
    }

    @Test
    public void testGetDevicesException() throws Exception {
        when(deviceService.getAllDevices()).thenThrow(new RuntimeException("Service exception"));

        mockMvc.perform(get("/devices")
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isInternalServerError())
                .andExpect(content().string("An error occurred, and handled: Service exception"));
    }
}

 

Explanation of the Test Components

  • @WebMvcTest(DeviceController.class) – This annotation configures the test to load only the web layer, specifically the DeviceController. It excludes the service and repository layers, making the test lightweight and focused only on HTTP request handling.

  • private MockMvc mockMvc;MockMvc is a Spring component that allows us to simulate HTTP requests and test the controller without starting a real web server.

  • @MockitoBean private DeviceService deviceService;@MockitoBean is a Spring Boot extension that automatically creates a mock instance of DeviceService and injects it into the test context. Since @WebMvcTest does not load service beans, mocking is necessary.

  • when(deviceService.getAllDevices()).thenReturn(...) – This sets up the expected behavior of the mocked service. Instead of calling the real getAllDevices method, it returns predefined test data.

  • mockMvc.perform(get("/devices")... – This simulates a GET request to /devices.

  • testGetDevicesException() – This test simulates an exception being thrown by the service. The GlobalExceptionHandler is expected to catch it and return a properly formatted error response.

This structured approach ensures that the controller works as expected, handles data correctly, and properly manages exceptions.

Conclusion

By using @WebMvcTest, we can efficiently test REST controllers without loading the full Spring context. The combination of MockMvc and MockitoBean allows us to isolate the web layer and validate API behavior. Since @WebMvcTest only loads MVC-related components, we must explicitly mock services and repositories. This approach ensures robust and maintainable tests for Spring Boot applications.