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 theDeviceController
. 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 ofDeviceService
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 realgetAllDevices
method, it returns predefined test data. -
mockMvc.perform(get("/devices")...
– This simulates aGET
request to/devices
. -
testGetDevicesException()
– This test simulates an exception being thrown by the service. TheGlobalExceptionHandler
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.