文档简介: 从 HTTP 契约层面验证 API:用 MockMvc 快速测控制器与校验规则,用 REST Assured 做贴近真实调用的验收测试,让接口行为成为可执行规格
目标读者: 需要为 REST 接口补自动化测试的开发者
前置知识: 已完成 集成测试;REST 设计见 Spring Boot 入门
文档信息(用途、难度与维护记录)
| 属性 | 内容 |
|---|---|
| 模块 | 08-java-revisited |
| 象限 | 操作指南 |
| 难度 | ⭐⭐ |
| 标签 | #MockMvc #RESTAssured #契约测试 |
| 更新日期 | 2026年9月 |
完成本文档后,你将能够:
- 用
@WebMvcTest+ MockMvc 独立验证控制器层 - 用 REST Assured 编写基于随机端口的验收测试
- 为接口建立"状态码 + 结构 + 业务语义"三层断言
@WebMvcTest(TodoController.class) // 只装配 MVC 层
class TodoControllerTest {
@Autowired MockMvc mockMvc;
@MockitoBean // Boot 3.4+:替换容器中的 Bean
TodoService todoService;
@Autowired JsonMapper jsonMapper; // Boot 4 / Jackson 3:复用主配置的 JsonMapper
@Test
@DisplayName("POST /api/todos 校验空白标题返回 400 与字段错误")
void rejectBlankTitle() throws Exception {
var body = jsonMapper.writeValueAsString(new TodoDto(null, " ", false));
mockMvc.perform(post("/api/todos")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isBadRequest()) // HTTP 层
.andExpect(jsonPath("$.title").value("标题不能为空")); // 响应结构
}
@Test
@DisplayName("GET 不存在的 id 返回 404 错误码")
void notFound() throws Exception {
given(todoService.get(999L)).willThrow(new TodoNotFoundException(999L));
mockMvc.perform(get("/api/todos/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.error").value("TODO_NOT_FOUND"));
}
@Test
@DisplayName("POST 成功返回 201 与完整资源")
void createSuccess() throws Exception {
given(todoService.create("学完 Java 21"))
.willReturn(new Todo(1L, "学完 Java 21", false, Instant.now()));
mockMvc.perform(post("/api/todos")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\":\"学完 Java 21\"}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.done").value(false));
}
}jsonPath()用 JSONPath 表达式断言响应体,$为根@WebMvcTest不连数据库:服务层用@MockitoBean替换(旧版为@MockBean)- 认证场景用
with(user("alice").roles("ADMIN"))模拟登录态
Boot 4 的依赖管理不再收录 REST Assured(仍可用,但需自行指定版本);官方新推的
RestTestClient是更轻的替代。
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.5.0</version> <!-- Boot 4 BOM 不再管理,需显式指定(以官方最新版为准) -->
<scope>test</scope>
</dependency>@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class TodoApiIT {
@LocalServerPort int port; // 随机端口:可并行、防冲突
@BeforeEach
void setUp() {
RestAssured.port = port;
}
@Test
@DisplayName("完整生命周期:创建 → 查询 → 删除")
void fullLifecycle() {
// given - when - then 以 HTTP 契约表达
var id =
given()
.contentType(ContentType.JSON)
.body("{\"title\":\"集成测试\"}")
.when()
.post("/api/todos")
.then()
.statusCode(201)
.body("title", equalTo("集成测试"))
.body("id", notNullValue())
.extract().jsonPath().getLong("id");
get("/api/todos/" + id)
.then()
.statusCode(200)
.body("done", is(false));
delete("/api/todos/" + id)
.then()
.statusCode(204);
}
}given()
.auth().oauth2(accessToken) // JWT 资源服务器的标准姿势
.header("X-Request-Id", UUID.randomUUID()) // 追踪键贯穿日志
.when()
.get("/api/admin/books")
.then()
.statusCode(200);| 层 | 断言内容 | 示例 |
|---|---|---|
| 状态码 | HTTP 语义 | statusCode(201) |
| 结构 | 字段存在与类型 | body("id", notNullValue()) |
| 语义 | 业务规则 | body("done", is(false)) |
状态码测试挂了说明契约破坏,结构测试挂了说明 DTO 变更未同步,语义测试挂了说明业务回归。
API 测试同时检查状态码、响应契约和持久化副作用。MockMvc 可在不同上下文中测试 MVC 链,不只等于“控制器单元”;真实端口测试则补充服务器与网络集成的边界。
创建后使用服务返回的 id,JSON 按字段语义断言而非依赖对象键序。为成功、无效输入、无身份和无权限分别设计用例,尤其验证失败操作没有修改数据;样例可以沉淀为接口文档但要随契约更新。
- API 跑通后打包交付 → Docker 部署
- 📖 Spring Boot 核心速查 — 测试自动配置
- 📖 故障排除速查 — 401/JSON 解析常见报错
- 📄 单元测试 — 服务层逻辑测试
- 📄 集成测试 — 真实依赖环境
- 📄 TODO API 项目 — 为其补 API 测试