测试 Spring Boot REST API 需要多个范围,因为直接的 Java 调用、模拟的 MVC 请求以及通过运行中服务器发出的请求,会分别验证不同的故障边界。有效的测试套件应将大多数行为检查放在 Spring 上下文之外,然后再添加范围更窄的 Web 切片测试和少量完整应用测试。
依赖示例面向 Spring Boot 3.x,因为其测试 starter 使用 JUnit 5。Spring Boot 4 管理 JUnit 6;Jupiter 的概念仍然适用,但若干 Boot 测试注解的包名发生了变化。
不使用测试框架进行测试 #
REST API 无需 JUnit、Spring Test 或 Mockito 也可以进行检查。启动应用,使用 JDK HTTP 客户端发送请求,并在响应违反契约时抛出错误:
package com.example;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public final class ManualApiCheck {
public static void main(String[] args) throws Exception {
var request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/greetings/Ada"))
.header("Accept", "application/json")
.GET()
.build();
var response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new AssertionError("status: " + response.statusCode());
}
if (!response.body().equals("{\"message\":\"Hello, Ada\"}")) {
throw new AssertionError("body: " + response.body());
}
}
}Java 的 assert 语句可以替代显式检查,但除非 JVM 使用 -ea 启动,否则断言处于禁用状态。因此,显式的 AssertionError 能够提供可靠的进程退出代码。
自定义可执行程序仍然适用于对已部署服务执行冒烟检查。完全替代测试框架还需要实现测试发现、fixture 生命周期、隔离、结果报告、并行执行和构建集成。手动的 curl 或浏览器检查提供的是探索能力,而不是可重复的回归测试套件。
选择测试范围 #
Spring 的测试文档区分了单元测试和集成测试,并提供了多个 Web 测试范围。不存在适用于每个端点的单一推荐注解。应选择包含待检查行为的最小范围:
| 范围 | 机制 | 所验证的行为 |
|---|---|---|
| 普通单元测试 | JUnit 和普通构造函数 | 不使用 Spring 的控制器或服务 Java 逻辑 |
| MVC 切片 | @WebMvcTest 和 MockMvc | 路由、绑定、验证、序列化、过滤器和控制器通知 |
| 带模拟 Web 层的完整上下文 | @SpringBootTest 和 @AutoConfigureMockMvc | 应用装配以及模拟的 Servlet 请求 |
| 完整运行服务器 | @SpringBootTest(webEnvironment = RANDOM_PORT) | 嵌入式服务器、过滤器、序列化、网络和完整应用装配 |
| 已部署系统测试 | 外部 HTTP 客户端 | 打包后的服务及其已部署的依赖项 |
Spring 建议在不需要 Spring 容器时使用单元测试。随后,MockMvc 可以在不运行服务器的情况下提供完整的 Spring MVC 请求处理。仍应保留一些运行服务器测试,因为模拟请求无法覆盖 Servlet 容器的网络、分发和错误页面行为。
使用数据库或 broker 的测试需要单独进行依赖决策:
- 测试应用决策时,将边界替换为 fake 或 mock。
- 如果数据库查询、映射、事务、broker 行为或线协议兼容性属于需求的一部分,则使用隔离的真实服务。
- 自动化测试绝不能指向生产基础设施。
Spring Boot 测试依赖 #
已经继承 Spring Boot 3 parent 或导入其 BOM 的应用只需要一个测试依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>该 starter 包含 Spring Boot Test、Spring Test、JUnit Jupiter、Mockito、AssertJ、Hamcrest、JSONassert、JsonPath 和 Awaitility。由于整个依赖集合作为同一个发行序列进行测试,因此应让 Spring Boot 管理它们的版本。
示例使用以下应用边界:
public interface GreetingService {
Greeting greet(String name);
}
public record Greeting(String message) {}
@RestController
@RequestMapping("/greetings")
public final class GreetingController {
private final GreetingService service;
public GreetingController(GreetingService service) {
this.service = service;
}
@GetMapping("/{name}")
Greeting get(@PathVariable String name) {
return service.greet(name);
}
}普通 JUnit 5 测试 #
Spring 组件仍然是普通 Java 对象。当测试关注分支、返回值或服务委托时,直接构造控制器可以避免启动上下文:
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class GreetingControllerTest {
@Test
void returnsTheServiceResult() {
GreetingService service =
name -> new Greeting("Hello, " + name);
var controller = new GreetingController(service);
assertEquals(
new Greeting("Hello, Ada"),
controller.get("Ada"));
}
}该测试不会验证 @GetMapping、路径变量转换、JSON 序列化、验证、过滤器或异常处理器,因为直接调用方法绕过了 Spring MVC。
使用 JUnit 5 的 MVC 切片测试 #
@WebMvcTest 会加载 MVC 基础设施和选定的 Web 组件,而不是完整应用。为控制器依赖提供确定性实现,从而将 Mockito 排除在测试之外:
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
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.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(GreetingController.class)
@Import(GreetingControllerWebTest.Dependencies.class)
class GreetingControllerWebTest {
@Autowired
MockMvc mvc;
@Test
void rendersAJsonGreeting() throws Exception {
mvc.perform(get("/greetings/Ada"))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(
"application/json"))
.andExpect(jsonPath("$.message").value("Hello, Ada"));
}
@TestConfiguration(proxyBeanMethods = false)
static class Dependencies {
@Bean
GreetingService greetingService() {
return name -> new Greeting("Hello, " + name);
}
}
}当安全配置、控制器通知、转换器或过滤器的行为属于端点契约时,应将它们添加到切片中。排除了待测生产组件的切片可能在组装后的应用失败时仍然通过,因此完整上下文覆盖仍然是必要的。
完整上下文测试 #
默认的 @SpringBootTest Web 环境是 MOCK。添加 @AutoConfigureMockMvc,即可通过完整应用上下文执行请求,而无需打开网络端口:
@SpringBootTest
@AutoConfigureMockMvc
class GreetingApplicationTest {
@Autowired
MockMvc mvc;
@Test
void servesAGreeting() throws Exception {
mvc.perform(get("/greetings/Ada"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("Hello, Ada"));
}
}当嵌入式服务器或真实 HTTP 行为属于测试范围时,应使用随机端口环境:
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class GreetingHttpTest {
@LocalServerPort
int port;
@Test
void servesAGreetingOverHttp() throws Exception {
var request = HttpRequest.newBuilder()
.uri(URI.create(
"http://localhost:" + port + "/greetings/Ada"))
.GET()
.build();
var response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode());
assertEquals(
"{\"message\":\"Hello, Ada\"}",
response.body());
}
}随机端口可以防止并发构建期间发生固定端口冲突。客户端和服务器代码运行在不同线程中,因此测试线程启动的事务不会自动回滚运行服务器请求提交的数据。应显式重置状态,或隔离后端服务。
Boot 3 还提供 TestRestTemplate,而 Spring 的响应式栈提供 WebTestClient。Spring Framework 7 添加了 RestTestClient,用于在 MockMvc 和运行服务器测试之间提供通用的同步断言 API。应选择应用所使用 Spring 版本支持的客户端,而不是直接复制未注明版本的示例。
不使用 Spring 上下文的 JUnit 5 和 Mockito #
当 Spring 配线不属于测试对象时,使用 MockitoExtension:
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class GreetingControllerMockitoTest {
@Mock
GreetingService service;
@InjectMocks
GreetingController controller;
@Test
void delegatesToTheService() {
when(service.greet("Ada"))
.thenReturn(new Greeting("Welcome, Ada"));
assertEquals(
new Greeting("Welcome, Ada"),
controller.get("Ada"));
verify(service).greet("Ada");
}
}该测试不会加载 Spring 上下文。@InjectMocks 会调用控制器构造函数,而 @Mock 会创建普通的 Mockito 对象。
Spring 测试上下文中的 Mockito #
普通的 @Mock 字段不会替换 Spring ApplicationContext 中的 Bean。当切片测试或完整上下文测试需要覆盖 Bean 时,应使用 @MockitoBean:
import static org.mockito.Mockito.verify;
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.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
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.test.web.servlet.MockMvc;
@WebMvcTest(GreetingController.class)
class GreetingControllerMockitoWebTest {
@Autowired
MockMvc mvc;
@MockitoBean
GreetingService service;
@Test
void rendersTheMockedServiceResult() throws Exception {
when(service.greet("Ada"))
.thenReturn(new Greeting("Welcome, Ada"));
mvc.perform(get("/greetings/Ada"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("Welcome, Ada"));
verify(service).greet("Ada");
}
}Spring Framework 的 TestContext 支持会创建 mock,并在刷新上下文之前替换匹配的 Bean。Spring Boot 测试注解已经注册了 SpringExtension,因此添加 MockitoExtension 无助于替换上下文 Bean。
当必须保留选定的真实行为时,使用 @MockitoSpyBean 包装现有的上下文 Bean。Spring Boot 较旧的 @MockBean 和 @SpyBean 注解在 Boot 3.4 中已弃用,改为推荐使用 Spring Framework 的 @MockitoBean 和 @MockitoSpyBean。使用 Boot 3.3 或更早版本的项目必须使用旧注解。
应将 Bean 覆盖应用于远程客户端、消息发布器、时钟或 repository 等架构边界。在完整上下文测试中替换大量内部 Bean,会移除选择 @SpringBootTest 所要验证的配线;此时,普通单元测试或 MVC 切片能够更准确地表达测试范围。
评论
在 GitHub 上阅读或评论(发表评论需要登录)