主题
界面
主题
接口测试的两个"前置难题":
① 接口要登录才能调 —— 测试里怎么"拿到登录态"?
② 接口操作数据 —— 数据从哪来?用完怎么办?这两个问题处理不好,测试就会"顺序依赖 + 互相污染" ✗(T1-007 的 I 原则被破坏)✓
先看"共享账号 + 共享数据"的典型故障:
测试 A:创建订单 ORD-1,断言列表有 1 条 ✓
测试 B:创建订单 ORD-2,断言列表有 2 条 ✓
CI 并行跑:A 与 B 同时执行 → 列表数量随机 → 两个测试随机红 ✗✗根因:数据是共享的、断言是绝对的 ✗ ——这就是 T1-007 的 I 原则被破坏的典型 ✓
每个受保护接口都要有这三类 ✓
| 类 | 请求 | 期望 | 验证什么 |
|---|---|---|---|
| ① 未认证 | 不带凭证 | 401 | 鉴权本身工作 ✓ |
| ② 错凭证 | 过期/伪造/格式错 token | 401 | 验签真的在校验 ★ |
| ③ 有效凭证 | 正确 token | 200 | 业务可达 ✓ |
★ 第 ② 类最容易被漏——很多人只测"没带"与"带了",却没测过"带了伪造的" ✗
实测(《Filter、Listener 与会话管理》 的 AuthFilter):
未带 token → 401 UNAUTHORIZED ✓
伪造 token → 401 UNAUTHORIZED ✓(校验逻辑在)
正确 token → 200 ✓再加一条"越权"用例 ★★:
| 场景 | 期望 |
|---|---|
| 普通用户调 ADMIN 接口 | 403(已认证但无权限)✓ |
401 与 403 的区别:《Spring Security 认证与授权》 的"认证 vs 授权" ✓ ——测试里要分开断言 ✓
三种方式:
| 方式 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| ① 调登录接口 ★★ | 测试里先 POST /login 拿 token | 走真实链路 ✓(连登录一起测了) | 依赖登录接口 ✓ |
| ② 直接构造 token | 测试里用 jwtService 签一个 | 快、可控(可造过期 token ★) | 绕过了登录逻辑 ⚠️ |
| ③ 复用公共账号 | 事先建好账号,测试里登录 | — | 共享状态 ✗ |
推荐:① 为主,② 为辅 ✓
| 场景 | 用 |
|---|---|
| 一般用例 | ① 调登录接口 ✓(把"登录可用"也纳入验证 ✓) |
| 要测"过期 token" | ② 直接签一个已过期的(调接口造不出来)★✓ |
| 要测"锁定" | ② 直接构造状态 ✓ |
封装成"登录辅助":
// support/TestAuth.java
public final class TestAuth {
public static String adminToken() {
return given().contentType(JSON)
.body("{\"username\":\"admin\",\"password\":\"admin-123\"}")
.when().post("/api/auth/login")
.then().statusCode(200)
.extract().jsonPath().getString("token");
}
}
// 用法:given().auth().preemptive().oauth2(TestAuth.adminToken()) ...★ 登录也是一次"接口调用" ——它失败了测试就该失败 ✓(登录坏了 = 一切都坏了,要尽早暴露 ✓)
模式:每个测试"自己准备、自己清理" ✓
@Test
void order_lifecycle() {
// Arrange:自建数据 ★
String orderNo = createOrder("P-1", 2);
// Act & Assert
given().when().get("/api/orders/" + orderNo)
.then().statusCode(200);
// Cleanup:自清 ★
deleteOrder(orderNo);
}为什么"自建"而不是"用预置数据":
| 自建 ★ | 预置数据 | |
|---|---|---|
| 可重复 | ✅ 每次都能跑 ✓ | 数据被上次改坏 → ✗ |
| 并行 | ✅ 各用各的 ✓ | 互相冲突 ✗ |
| 意图清晰 | ✅ 数据就在用例里 ✓✓ | 要翻数据脚本 ⚠️ |
★ "自建"的最简实现:每次用唯一标识 ★
String suffix = UUID.randomUUID().toString().substring(0, 8);
String username = "test-user-" + suffix; // ✓ 不会与并行运行的测试冲突 ✓这与 T1-017 的"不依赖具体数据条数"配合:自建 + 相对断言 = 完整隔离 ✓
| 方式 | 做法 | 风险 |
|---|---|---|
| ① 用例末尾清理 | @AfterEach 删自建数据 ✓ | 失败时可能跳过 ⚠️ → 用 try/finally ✓ |
| ② 数据库重置 | 每个测试后 TRUNCATE | 慢;多实例不友好 ⚠️ |
| ③ 定期清库 | 每晚清一次测试库 | 白天会累积 ⚠️(可接受) |
★ 推荐 ① + ③:平时自清,攒了再定期清 ✓
⚠️ @AfterEach 里清理要"容错":
@AfterEach
void cleanup() {
try {
deleteOrder(orderNo); // ✗ 如果 orderNo 为 null(测试没走到创建)会 404 ✗
} finally {
orderNo = null;
}
}
// ✓ 更稳:if (orderNo != null) deleteOrder(orderNo);并行测试 + 唯一约束(如 username unique)的场景:
// ✗ 两个测试都建 "test-user" → 第二个 409 ✗
// ✓ 每个测试用唯一名
String username = "user-" + UUID.randomUUID().toString().substring(0, 8);这与 《Linux 文件、用户与权限管理》 的"唯一索引带 deleted"、T1-017 的"数据隔离"是同一类问题 ✓
| 原因 | 说明 |
|---|---|
| 合规 ✗ | 用户隐私数据进测试日志 ✗✗ |
| 破坏生产 ✗ | 测试的写入/删除会真实生效 ✗✗✗ |
| 不可重复 | 生产数据时刻在变 ✗ |
正确做法:独立测试环境 + 脱敏数据 ✓ ——且用"造数据脚本"而不是"复制生产" ✓
这与 《Spring Boot 配置管理与多环境》 的"配置隔离"、《Linux 服务部署与运维》 的"应用用专用用户"是同一条"环境分离"原则 ✓
String token = login("admin", "admin-123");
given().auth().preemptive().oauth2(token)
.when().get("/api/admin/stats").then().statusCode(200);见第 7 节 ✓
static String sharedOrderNo; // ✗ 测试顺序依赖 ✗✗
@Test void create() { sharedOrderNo = ...; }
@Test void query() { get("/orders/" + sharedOrderNo); } // ✗ 依赖 create 先跑修:合并成一个"生命周期测试"(T2-005 的 full_lifecycle ✓)——"依赖"发生在同一个测试内部就合法 ✓
文件:labs/spring-boot-web/src/test/java/cn/kb/learn/web/OrderAuthAndDataTest.java
@SpringBootTest(webEnvironment = RANDOM_PORT)
@DisplayName("鉴权与测试数据管理")
class OrderAuthAndDataTest {
@LocalServerPort int port;
@Autowired ObjectMapper om;
@BeforeEach void setUp() { RestAssured.port = port; }
private String createOrder() {
return given().contentType(JSON)
.body("{\"productId\":\"P-T\",\"quantity\":1,\"address\":\"测试地址\"}")
.when().post("/api/orders")
.then().statusCode(201)
.extract().jsonPath().getString("orderNo");
}
@Test @DisplayName("① 未认证可访问(本项目接口未配鉴权,演示用例形态)")
void unauthenticated_access() {
String orderNo = createOrder(); // 自建数据:ORD-1 可能已被其他用例删除,不能依赖它存在
given().when().get("/api/orders/" + orderNo).then().statusCode(anyOf(is(200), is(401)));
// ★ 受保护接口的期望是 401 —— 这里用 anyOf 说明"断言要与系统的安全模型一致" ✓
}
@Test @DisplayName("② 自建自清:创建 → 查询 → 删除 → 确认删除")
void self_contained_data() {
String orderNo = createOrder(); // 自建 ★
given().when().get("/api/orders/" + orderNo).then().statusCode(200);
given().when().delete("/api/orders/" + orderNo).then().statusCode(204);
given().when().get("/api/orders/" + orderNo).then().statusCode(404); // 已清 ✓
}
@Test @DisplayName("③ 唯一数据:两次创建生成不同 orderNo")
void unique_data_per_request() {
String a = createOrder();
String b = createOrder();
assertThat(a).isNotEqualTo(b); // ★ 唯一性 ✓
}
}运行:
cd labs
.\mvnw.cmd -pl spring-boot-web test "-Dtest=OrderAuthAndDataTest"预期:3 个测试全绿 ✓(总测试数增至 121)✓
-T 1C 或 JUnit 并行配置):观察"自建自清"的用例不再互相污染 ★✓;| # | 错误 | 说明 |
|---|---|---|
| 1 | 静态变量共享测试数据 ✗✗ | 顺序依赖(6.3)✗ |
| 2 | 断言绝对数量(total == 5)✗ | 改用 >= 或自建后断言 ✓ |
| 3 | @AfterEach 清理抛 NPE | 加 null 判断 / try-finally ✓ |
| 4 | 对生产环境跑接口测试 ✗✗✗ | 绝对禁止(5.6)✗ |
| 5 | 测试账号密码硬编码进代码 | 用环境变量 / 测试配置(《Spring Boot 配置管理与多环境》)✓ |
-Djunit.jupiter.execution.parallel.enabled=true),观察是否仍稳定 ★(这正是"自建自清"的收益)✓;扩展任务:写 TestDataFactory:createOrder(String productId, int qty) / deleteOrder(String no)——让所有测试用工厂建数据,统计"测试里直接写请求体的次数"下降了多少 ✓
total == 1 与 total == 2,为什么并行时必挂?《接口依赖、幂等性与契约测试》:接口之间的"先后关系"怎么处理,"重复调用"怎么保证安全。
暂无