主题
界面
主题
单元测试(T1-001 ~ T1-012)验证的是一个类;接口测试验证的是一个契约:
单元测试: 给方法这些参数 → 返回这个值
接口测试: 发这个请求(方法/路径/头/体)→ 应该得到这个状态码、这个结构、这些业务结果本节不讲工具(《REST Assured 与 Java HTTP Client》),先讲"设计"——因为"测什么"永远比"怎么发请求"重要 ✓
先看"想到哪测到哪"的典型结果:
测试了:正常创建、正常查询、正常删除 ← 都过了 ✓
上线后:
· 有人传 quantity=0 的订单 → 创建成功 → 负数库存 ✗
· 有人用 GET 删除接口 → 405 却没有提示语义 ✗
· 重复点击"提交" → 生成了两笔订单 ✗✗问题不在"没测",在"没有系统的设计" ✗ ——接口测试是"设计出来的",不是"写出来的" ✓
| 层次 | 验证什么 | 例子 |
|---|---|---|
| ① 契约层 ★ | 状态码、结构、头与文档一致 | 201 必须带 Location ✓ |
| ② 功能层 ★ | 业务行为正确 | 满 2 件打 9 折 → 金额对 ✓ |
| ③ 异常层 ★★ | 非法输入被正确拒绝 | quantity=0 → 422 + 逐字段错误 ✓ |
| ④ 安全层 ★★ | 鉴权、越权、注入被挡住 | 未登录 → 401;普通用户删单 → 403(《Spring Security 认证与授权》)✓ |
比例参考:①② 约 30%,③ 约 50%,④ 约 20% ✓ ——异常与安全用例的数量通常是正向的 2~3 倍 ⚠️(新手常见误区是 90% 都是正向 ✗)
每条用例至少覆盖前两类,关键用例覆盖四类 ✓
| 断言类型 | 断什么 | 例子 |
|---|---|---|
| ① 状态码 ★ | 语义是否正确 | 创建 201、删除 204、校验失败 422 ✓ |
| ② 响应结构 ★ | 字段存在与类型 | details 是数组且长度 2 ✓ |
| ③ 业务字段 ★★ | 具体业务值 | amountInCents = 39800(不是只查"有值")✓ |
| ④ 错误契约 ★★ | code + traceId + details | code = VALIDATION_FAILED ✓(《参数校验与全局异常处理》) |
⚠️ 一个常见不足:只断言状态码 ✗
then().statusCode(201); // ✗ 不够——返回什么字段?金额对不对?Location 有吗?后果:接口改坏了业务逻辑(如金额算错)但状态码仍是 201 → 测试照样绿 ✗✗
以本项目的"创建订单"接口为例(《参数校验与全局异常处理》 的 DTO):
productId: @NotBlank
quantity: @Min(1) @Max(100)
address: @NotBlank @Size(max = 200)第一步:为每个字段划分等价类与边界
| 字段 | 有效等价类 | 无效等价类 | 边界 |
|---|---|---|---|
| productId | 非空字符串 | null / 空串 / 纯空白 | — |
| quantity | 1~100 的整数 | 0、负数、101、非整数、非数字 | 0 / 1 / 100 / 101 ★ |
| address | 1~200 字符 | 空 / 201 字符 | 200 / 201 ★ |
| 请求体 | 合法 JSON | 非法 JSON / 空体 / 无 Content-Type | — |
| Content-Type | application/json | text/plain / 缺失 | — |
第二步:组合成用例矩阵 ★★★
| # | 用例 | 请求要点 | 期望 | 类别 |
|---|---|---|---|---|
| 1 | 正常创建 | 三字段合法 | 201 + Location + 字段回显 | 正向 |
| 2 | quantity 下边界 =1 | min | 201 ✓ | 边界 |
| 3 | quantity 上边界 =100 | max | 201 ✓ | 边界 |
| 4 | quantity =0 | min-1 | 422 + quantity 错误 | 边界 ✗ |
| 5 | quantity =101 | max+1 | 422 | 边界 ✗ |
| 6 | quantity =-5 | 负数 | 422 | 反向 |
| 7 | quantity =1.5 | 非整数 | 400(类型转换,《Spring MVC 请求映射与参数绑定》)✓ | 反向 |
| 8 | quantity ="abc" | 非数字 | 400 | 反向 |
| 9 | productId 空串 | @NotBlank | 422 | 反向 |
| 10 | productId 纯空白 | " " | 422(@NotBlank 的意义)✓ | 反向 |
| 11 | address 201 字符 | max+1 | 422 | 边界 ✗ |
| 12 | address 200 字符 | max | 201 ✓ | 边界 |
| 13 | 三个字段都非法 | 一次性全错 | 422 + details=3 条 ★ | 反向组合 |
| 14 | 请求体非法 JSON | {"x | 400(《JSON 与 XML》 的 HttpMessageNotReadableException) | 反向 |
| 15 | 缺 Content-Type | 默认表单 | 415 ✗ | 反向 |
| 16 | 空 JSON {} | 全缺 | 422 + 每字段一条错误 ★ | 反向 |
| 17 | 未认证 | 无 token | 401(《Spring Security 认证与授权》) | 安全 |
| 18 | 重复提交两次 | 相同请求 | 201 + 201(幂等性?→ T2-007)⚠️ | 特殊 |
★ 这张矩阵就是 T2-001 的核心产出 ✓ ——18 条用例覆盖了 5.1 的全部四层 ✓
用例 13 是新手最容易漏的:"一次提交多个错误"——它验证的是错误收集机制(《参数校验与全局异常处理》 的"Bean Validation 收集全部错误")✓
经验值:反向 : 正向 ≈ 2:1 ~ 3:1 ✓
| 为什么反向更多 | 说明 |
|---|---|
| 每个正向输入对应 N 个非法变体 | 一个 int 字段有:空、非数字、超界、溢出……✓ |
| 正向路径通常被" happy path"覆盖 | 而异常路径只有刻意设计才会覆盖 ⚠️ |
| 线上事故多来自异常路径 ✗ | 用户永远会输入你没想到的东西 ✓ |
⚠️ 但也要防"为反向而反向":
"quantity=999999999999" 与 "quantity=101" 是同一个等价类(超上界) ——不需要都测 ✓
同一等价类取一个代表即可 ✓(等价类方法的全部意义就在于此 ✓)
| 来源 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| ① 用例内构造 ★ | 测试里自己建数据(如 《Vue 基础与组件开发》 的建单→查询)✓ | 自包含、可重复 ✓✓ | 代码量大 |
| ② 共享测试数据 | 准备一批"公共数据" | 快 | 互相污染 ✗(T1-017 的教训) |
| ③ 随机生成 | 每次随机 | 覆盖广 | 难复现失败 ✗(要配合 seed 记录) |
★ 推荐组合:① 为主——每个测试"自己创建、自己断言、自己清理" ✓
这与 T1-017 的"测试数据隔离"是同一条原则 ✓
| 问题 | 单元测试 | 接口测试 |
|---|---|---|
| 金额计算的五种分支 | ✅ 单元测试(毫秒级)✓ | ❌ 不用(T1-017 6.3)✗ |
| 接口的参数校验规则 | ❌(校验由框架做) | ✅ 切片/接口测试(T1-016)✓ |
| 跨层协作(Controller→Service→存储) | ❌ | ✅ 接口测试 ✓ |
| 业务规则(如"商品是否存在") | ✅ Service 单元测试(mock 数据层)✓ | ⚠️ 集成测试抽 1~2 条即可 |
判断标准(与 T1-017 的"该写集成测试吗"一致):
"这个验证需要真实的 HTTP 与完整的链路吗?"
- 需要(路由、绑定、校验、序列化、跨层)→ 接口测试 ✓
- 不需要(纯计算、纯分支)→ 单元测试 ✓
接口测试的数量级 = 接口数 × 关键场景(不是"代码分支数")✓
given().contentType(JSON).body(合法请求)
.when().post("/api/orders")
.then().statusCode(201)
.header("Location", containsString("/api/orders/")) // ② 结构
.body("orderNo", startsWith("ORD-")) // ③ 业务
.body("status", equalTo("CREATED")); // ③ 业务见 5.3 的 18 条 ✓
// ✗ "测试完成"——但异常路径 0% 覆盖
@Test void testCreate() { ...正常创建... }见 5.3 的 18 条用例表 ✓ ——它是本节的核心交付物 ✓
落地方式:矩阵可以直接变成参数化测试(T1-002 的 @MethodSource)✓
static Stream<Arguments> invalidRequests() {
return Stream.of(
Arguments.of("quantity=0", """
{"productId":"P","quantity":0,"address":"上海"}""", "quantity", "数量必须大于 0"),
Arguments.of("productId 空白", """
{"productId":" ","quantity":1,"address":"上海"}""", "productId", "商品编号不能为空")
// ……与矩阵一一对应 ✓
);
}★ "设计矩阵 → 参数化测试"就是从设计到代码的最短路径 ✓
| # | 错误 | 说明 |
|---|---|---|
| 1 | 只测正向 ✗ | 异常路径才是事故高发区(5.1)✗ |
| 2 | 只断言状态码 ✗ | 业务算错但状态码对 → 测试照样绿 ✗✗(5.2) |
| 3 | 不测组合(多字段同时非法) | 错误收集机制没有验证 ⚠️ |
| 4 | 测试数据互相依赖 ✗ | 顺序化测试 = 脆弱(T1-007 的 I 原则)✗ |
| 5 | 忘了幂等/重复提交类用例 | "重复点击提交"是真实场景 ★(T2-007) |
| 6 | 把单元测试的用例搬到接口层 | 重复且慢(5.6)✗ |
目标:亲手为"订单接口"完成 18 条用例矩阵的设计与实现。
步骤:
@MethodSource)✓;@Min(1) 删掉 → 观察哪些用例变红 ★ ——这就是"测试在保护你"的现场 ✓;验收方式:能说出第 4 步"删掉 @Min 后哪条用例红了、它断言的是什么"。
扩展任务:为"查询订单列表"接口(分页 + 状态筛选)设计用例矩阵:
基础题
代码阅读题
public record CreateUserRequest(
@NotBlank @Size(min = 4, max = 20) String username,
@NotBlank @Size(min = 8, max = 32) @Pattern(regexp = ".*\\d.*") String password,
@Email String email
) { }password 字段设计全部边界与无效等价类用例(列出请求与期望)。编程题
POST /api/articles(创建,标题唯一→409);综合任务
提示 1(方向):第 1 题的方向是"正向只是很小的一部分"。
提示 2(关键点):第 5 题——"min 8 位"与"必须含数字"是两个独立的规则,各有各的边界。
提示 3(接近答案):第 7 题"接口测试先行"——契约(《接口文档与前后端联调》)定了,用例矩阵就能先于实现完成 ✓ 这正是"测试左移"的落地形态。
基础题
代码阅读题
| 用例 | 请求值 | 期望 |
|---|---|---|
| 下边界 =8 位 | "12345678" | 通过(若含数字 ✓) |
| 上边界 =32 位 | 32 字符含数字 | 通过 ✓ |
| 7 位 | "1234567" | 422:长度 8~32 ✗ |
| 33 位 | 33 字符 | 422 ✗ |
| 8 位但无数字 | "abcdefgh" | 422:必须含数字 ✗ |
| 空串 | "" | 422:NotBlank ✗ |
| null | 缺字段 | 422 ✗ |
| 纯空白 | " " | 422 ✗ |
★ 注意"两个条件的组合":"8 位含数字"通过、"8 位无数字"失败、"7 位含数字"失败 ——多条规则叠加时,组合用例最容易漏 ⚠️
编程题
综合任务
| 项 | 内容 |
|---|---|
| 检查清单 | ① 有没有等价类表 ② 边界两侧都测了吗 ③ 组合非法用例 ④ 错误契约断言 ⑤ 幂等/重复 ⑥ 鉴权 ⑦ 数据自包含 ⑧ 反向比例 ≥ 正向 |
| 质量度量 | "每千行代码发现的接口级缺陷数" 与 "异常路径用例占比" ✓ |
| 测试先行 | 契约(OpenAPI)→ 用例矩阵 → 实现 ——测试先红后绿 ✓(T1-006 的 TDD 在接口层的形态) |
| 矩阵进文档 | 矩阵是"契约的可执行形式" ——它比"描述性文档"更不容易过期 ✓(《接口文档与前后端联调》) |
| 与测试左移的关系 | 测试左移 = 把测试活动提前到设计与开发阶段——"接口测试先行"就是它在接口层的具体形态 ✓ |
| ::: |
《REST Assured 与 Java HTTP Client》:把这些用例矩阵变成自动化代码。