Skip to content

接口鉴权与测试数据管理 ​

T2-006必学进阶约 60 分钟JDK 21测试专项 B:接口·UI·性能 · 接口自动化

1. 本节定位 ​

接口测试的两个"前置难题":

text
① 接口要登录才能调 —— 测试里怎么"拿到登录态"?
② 接口操作数据 —— 数据从哪来?用完怎么办?

这两个问题处理不好,测试就会"顺序依赖 + 互相污染" ✗(T1-007 的 I 原则被破坏)✓

2. 学习目标 ​

  • 设计鉴权的三类用例(未认证 / 错凭证 / 有效凭证)★★;
  • 说出"测试里获取登录态"的三种方式与取舍 ★★;
  • 掌握"自建自清"的数据隔离模式 ★★★;
  • 会用"唯一数据"避免并发冲突(如时间戳后缀)★;
  • 知道为什么绝不能对生产数据做测试 ✗✗。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看"共享账号 + 共享数据"的典型故障:

text
测试 A:创建订单 ORD-1,断言列表有 1 条 ✓
测试 B:创建订单 ORD-2,断言列表有 2 条 ✓
CI 并行跑:A 与 B 同时执行 → 列表数量随机 → 两个测试随机红 ✗✗

根因:数据是共享的、断言是绝对的 ✗ ——这就是 T1-007 的 I 原则被破坏的典型 ✓

5. 核心概念 ​

5.1 鉴权的三类用例 ★★ ​

每个受保护接口都要有这三类 ✓

类请求期望验证什么
① 未认证不带凭证401鉴权本身工作 ✓
② 错凭证过期/伪造/格式错 token401验签真的在校验 ★
③ 有效凭证正确 token200业务可达 ✓

★ 第 ② 类最容易被漏——很多人只测"没带"与"带了",却没测过"带了伪造的" ✗

实测(《Filter、Listener 与会话管理》 的 AuthFilter):

text
未带 token  → 401 UNAUTHORIZED ✓
伪造 token  → 401 UNAUTHORIZED ✓(校验逻辑在)
正确 token  → 200 ✓

再加一条"越权"用例 ★★:

场景期望
普通用户调 ADMIN 接口403(已认证但无权限)✓

401 与 403 的区别:《Spring Security 认证与授权》 的"认证 vs 授权" ✓ ——测试里要分开断言 ✓

5.2 测试里怎么"获得登录态" ★★ ​

三种方式:

方式做法优点缺点
① 调登录接口 ★★测试里先 POST /login 拿 token走真实链路 ✓(连登录一起测了)依赖登录接口 ✓
② 直接构造 token测试里用 jwtService 签一个快、可控(可造过期 token ★)绕过了登录逻辑 ⚠️
③ 复用公共账号事先建好账号,测试里登录—共享状态 ✗

推荐:① 为主,② 为辅 ✓

场景用
一般用例① 调登录接口 ✓(把"登录可用"也纳入验证 ✓)
要测"过期 token"② 直接签一个已过期的(调接口造不出来)★✓
要测"锁定"② 直接构造状态 ✓

封装成"登录辅助":

java
// 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()) ...

★ 登录也是一次"接口调用" ——它失败了测试就该失败 ✓(登录坏了 = 一切都坏了,要尽早暴露 ✓)

5.3 测试数据管理:自建自清 ★★★ ​

模式:每个测试"自己准备、自己清理" ✓

java
@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);
}

为什么"自建"而不是"用预置数据":

自建 ★预置数据
可重复✅ 每次都能跑 ✓数据被上次改坏 → ✗
并行✅ 各用各的 ✓互相冲突 ✗
意图清晰✅ 数据就在用例里 ✓✓要翻数据脚本 ⚠️

★ "自建"的最简实现:每次用唯一标识 ★

java
String suffix = UUID.randomUUID().toString().substring(0, 8);
String username = "test-user-" + suffix;       // ✓ 不会与并行运行的测试冲突 ✓

这与 T1-017 的"不依赖具体数据条数"配合:自建 + 相对断言 = 完整隔离 ✓

5.4 "清理"的三种方式 ★ ​

方式做法风险
① 用例末尾清理@AfterEach 删自建数据 ✓失败时可能跳过 ⚠️ → 用 try/finally ✓
② 数据库重置每个测试后 TRUNCATE慢;多实例不友好 ⚠️
③ 定期清库每晚清一次测试库白天会累积 ⚠️(可接受)

★ 推荐 ① + ③:平时自清,攒了再定期清 ✓

⚠️ @AfterEach 里清理要"容错":

java
@AfterEach
void cleanup() {
    try {
        deleteOrder(orderNo);        // ✗ 如果 orderNo 为 null(测试没走到创建)会 404 ✗
    } finally {
        orderNo = null;
    }
}
// ✓ 更稳:if (orderNo != null) deleteOrder(orderNo);

5.5 唯一性与并发 ★ ​

并行测试 + 唯一约束(如 username unique)的场景:

java
// ✗ 两个测试都建 "test-user" → 第二个 409 ✗
// ✓ 每个测试用唯一名
String username = "user-" + UUID.randomUUID().toString().substring(0, 8);

这与 《Linux 文件、用户与权限管理》 的"唯一索引带 deleted"、T1-017 的"数据隔离"是同一类问题 ✓

5.6 绝不用生产数据测试 ✗✗ ​

原因说明
合规 ✗用户隐私数据进测试日志 ✗✗
破坏生产 ✗测试的写入/删除会真实生效 ✗✗✗
不可重复生产数据时刻在变 ✗

正确做法:独立测试环境 + 脱敏数据 ✓ ——且用"造数据脚本"而不是"复制生产" ✓

这与 《Spring Boot 配置管理与多环境》 的"配置隔离"、《Linux 服务部署与运维》 的"应用用专用用户"是同一条"环境分离"原则 ✓

6. 递进示例 ​

6.1 最小示例 ​

java
String token = login("admin", "admin-123");
given().auth().preemptive().oauth2(token)
       .when().get("/api/admin/stats").then().statusCode(200);

6.2 业务示例:三类鉴权用例 ​

见第 7 节 ✓

6.3 反例:把"上一步的结果"存在静态变量 ​

java
static String sharedOrderNo;        // ✗ 测试顺序依赖 ✗✗

@Test void create() { sharedOrderNo = ...; }
@Test void query()  { get("/orders/" + sharedOrderNo); }   // ✗ 依赖 create 先跑

修:合并成一个"生命周期测试"(T2-005 的 full_lifecycle ✓)——"依赖"发生在同一个测试内部就合法 ✓

7. 完整可运行代码 ​

文件:labs/spring-boot-web/src/test/java/cn/kb/learn/web/OrderAuthAndDataTest.java

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);                        // ★ 唯一性 ✓
    }
}

运行:

powershell
cd labs
.\mvnw.cmd -pl spring-boot-web test "-Dtest=OrderAuthAndDataTest"

预期:3 个测试全绿 ✓(总测试数增至 121)✓

8. Debug 观察点 ​

  1. 并行跑(-T 1C 或 JUnit 并行配置):观察"自建自清"的用例不再互相污染 ★✓;
  2. 删掉 cleanup:多次跑后看数据累积 ⚠️;
  3. 故意用固定用户名建两个用户:观察唯一约束的 409 ✓。

9. 常见错误 ​

#错误说明
1静态变量共享测试数据 ✗✗顺序依赖(6.3)✗
2断言绝对数量(total == 5)✗改用 >= 或自建后断言 ✓
3@AfterEach 清理抛 NPE加 null 判断 / try-finally ✓
4对生产环境跑接口测试 ✗✗✗绝对禁止(5.6)✗
5测试账号密码硬编码进代码用环境变量 / 测试配置(《Spring Boot 配置管理与多环境》)✓

10. 动手实验 ​

  1. 跑第 7 节的 3 个测试 ✓;
  2. 把三条测试改为并行(-Djunit.jupiter.execution.parallel.enabled=true),观察是否仍稳定 ★(这正是"自建自清"的收益)✓;
  3. 加一个"越权用例":普通用户调管理接口断言 403(需先给 spring-boot-web 加角色,《Spring Security 认证与授权》)✓。

扩展任务:写 TestDataFactory:createOrder(String productId, int qty) / deleteOrder(String no)——让所有测试用工厂建数据,统计"测试里直接写请求体的次数"下降了多少 ✓

11. 练习题 ​

  1. 判断:只要带了 token,任何接口都能调。(对 / 错)
  2. 填空:鉴权三类用例是 、、______;数据隔离的核心模式是 ______。
  3. 简答:为什么"测试里调登录接口拿 token"优于"手工构造 token"?
  4. (代码阅读)两个测试分别断言 total == 1 与 total == 2,为什么并行时必挂?
  5. (编程)实现"错误密码返回 401"与"正确密码返回 200"两条用例(《Spring Security 认证与授权》)。
参考答案(要点)
  1. 错。还要有对应角色/权限 ——401(未认证)与 403(未授权)是两道门 ✓(《Spring Security 认证与授权》)★
  2. 未认证(401)、错凭证(401)、有效凭证(200);自建自清 ✓
  3. 走真实链路——连"登录接口本身"一起验证 ✓;且与生产行为一致 ✓(手工构造可能"与真实签发逻辑不符" ⚠️)
  4. 共享存储 + 绝对断言 + 并行执行 = 竞态 ✗✗——两个测试互相改变对方的"期望值" ✓
  5. 要点:401 是"业务正常流程"(《日志框架与日志规范》 的级别判断:不需要人介入,用 INFO)✓

12. 掌握标准 ​

  • [ ] 能为每个受保护接口设计三类鉴权用例 ★★;
  • [ ] 掌握"自建自清"的数据隔离模式 ★★★;
  • [ ] 知道"测试里获取登录态"的三种方式与取舍;
  • [ ] 知道并行测试对数据唯一性的要求 ★;
  • [ ] 绝不使用生产数据测试 ✗✗。

13. 扩展资源 ​

14. 下一步学习 ​

《接口依赖、幂等性与契约测试》:接口之间的"先后关系"怎么处理,"重复调用"怎么保证安全。

后续知识

暂无

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引