主题
界面
主题
测试的价值不在"跑过",而在**"结果被正确解读、失败被正确处理"** ✓
三件小事决定测试体系的生死:
| 事 | 做对 | 做错 |
|---|---|---|
| 报告 | 一眼看出"什么坏了、坏在哪" ✓ | 数字一堆,没人看 ✗ |
| 失败分类 | 产品 bug / 测试 bug / 环境——各修各的 ✓ | 全部"再跑一遍" ✗ |
| flaky 处理 | 修到稳定 ✓✓ | "重跑就绿"成为习惯 → 测试失效 ✗✗✗ |
一个真实的滑坡过程:
第 1 周:CI 有 1 条不稳定测试 → "重跑一次就绿了,没事"
第 1 月:不稳定测试累积到 5 条 → "CI 红了先重跑再说" ✗
第 2 月:真 bug 出现 → CI 红 → 大家习惯性重跑 → "哦,还是红的" → 忽略 ✗✗
第 3 月:事故发生 → 复盘发现"CI 其实早就红了,只是没人看" ✗✗✗根因:第一条 flaky 没有被当回事 ✗
测试体系的价值 = 单个测试的可信度 × 团队对结果的信任 ✓
flaky 是在同时摧毁这两个因子 ✗✗✗
| 层次 | 内容 | 受众 |
|---|---|---|
| 单条测试 | 哪个用例、断言什么、为什么失败 | 写测试的人 ✓ |
| 批次报告 | 通过率、耗时分布、失败分布(按模块/类型) | 团队(每日站会)✓ |
| 趋势 | 缺陷发现数、flaky 数量、覆盖变化 | 管理者(T1-018)⚠️ |
surefire 报告的解读(回顾 T1-014):
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 21.85 s两种失败要分开 ★:
| 类型 | 在报告里 | 含义 |
|---|---|---|
| Failures | 断言失败 | 代码跑了,结果不符合期望 ★ |
| Errors | 抛了异常 | 代码根本没跑到断言(NPE、连接失败)⚠️ |
排查方向完全不同:
Failures → 先看"期望 vs 实际"——是断言错了还是实现错了?
Errors → 先看异常堆栈——通常在到达断言之前就炸了(环境/数据/NPE)✓失败的五种分类(每一类有自己的处理路径):
| 分类 | 特征 | 谁修 | 修什么 |
|---|---|---|---|
| ① 产品 bug ★ | 断言失败,但测试的期望是对的 | 开发 | 修产品 + 加回归说明 ✓ |
| ② 测试 bug | 测试本身写错(路径、参数、断言) | 测试作者 | 修测试 ✓ |
| ③ 环境 | 本地过、CI 挂(或全员挂) | 运维/开发 | 恢复环境 ✓ |
| ④ 数据过期 | 依赖的预置数据变了 | 测试作者 | 改自建自清(T2-006)✓ |
| ⑤ flaky(不稳定) | 同一代码时红时绿 ✗✗ | 测试作者 | 隔离变量并修根因 ★★★ |
分类的第一个动作:
"这条失败,在别人机器上、在 CI 上、重跑一次,还失败吗?" ★
答案 分类 全都失败 ① 或 ②(逻辑问题)✓ 只有 CI 失败 ③(环境差异)✓ 有时失败 ⑤(flaky)⚠️ 曾经过、现在挂 ④ 或 ①(什么变了?)★
① 复现 ★
在本地跑一次失败的用例(单独跑,不带其他)
→ 能复现?→ 走逻辑分析
→ 不能? → 环境或 flaky(③⑤)
② 分类(5.2 的五选一)
③ 定位
Failures → 看"期望 vs 实际"的具体差异
Errors → 看异常堆栈的最上层(Caused by)★
④ 修复
① → 修产品代码 + 补回归说明 ✓
② → 修测试
③/④ → 修环境/数据(并把"依赖"写进测试注释 ✓)
⑤ 防复发 ★★★
这次为什么没拦住?
· 校验规则缺了 → 补规则(S3-032)
· 用例没覆盖 → 补用例([T2-001](./T2-001) 的矩阵)
· 人工步骤忘了 → 自动化(S3-035)★ 第 ⑤ 步是"测试的测试":每一次失败都应该让体系更强一点 ✓
这与 《Spring Boot 配置管理与多环境》 综合任务的"把事故变成检查"完全一致 ✓
定义:相同代码、相同环境,结果时红时绿 ✗
常见根因与修法:
| # | 根因 | 修法 |
|---|---|---|
| 1 | 测试顺序依赖 ✗ | 每个测试自建数据(T2-006)✓ |
| 2 | 并发/时序(sleep、异步) | 用"重试+超时"的等待(如 Awaitility 思路)✓ 而不是固定 sleep ✗ |
| 3 | 依赖外部系统 ✗ | mock / Testcontainers(T1-017)✓ |
| 4 | 共享时间/随机数 | 注入时钟、记录 seed ✓ |
| 5 | 资源冲突(同端口、同名文件) | 随机化(T2-006 的 UUID 后缀)✓ |
处理策略:
| 策略 | 说明 |
|---|---|
| 立即修 ★ | 首选 ✓ |
| 隔离区(quarantine) ⚠️ | 暂时移出主 CI,单独跑 + 限期修复 ✓ |
| "重跑 N 次取绿" ✗✗✗ | 这不是修复,是掩盖 ✗ ——它会杀死整个测试体系 ✗✗✗ |
★ "重跑取绿"为什么致命:
它会训练出"CI 红了就重跑"的肌肉记忆 ——真正的 bug 也会被"重跑掉" ✗✗
信任一旦丢失,测试体系就死了 ✗✗✗
宁可暂时移出 flaky(并挂牌限期修),也不要让它污染主 CI ✓
最小复现 = 定位的一半 ✓
| 手段 | 说明 |
|---|---|
| 单独跑失败用例 | mvn test -Dtest=Xxx#method(排除其他测试的干扰)✓ |
| 固定随机 | 打印并复用 seed ✓ |
| 缩小数据 | 造最小的复现数据(而不是复用整库)✓ |
| 记录环境 | 端口、依赖版本、时间 ✓ |
一个实用技巧:失败时让测试输出"足够的上下文" ——请求体、响应体、traceId(《日志框架与日志规范》)✓ ——否则"下次就复现不了" ✗
| 受众 | 他们关心 | 报告要给什么 |
|---|---|---|
| 开发 | 哪个用例挂了、为什么 | 失败详情 + 上下文 + 复现命令 ✓ |
| 测试 | 哪些场景没覆盖 | 覆盖与用例矩阵的差距 ✓ |
| 团队 | "现在能不能发" | 通过率 + 失败清单 ✓ |
| 管理者 | 质量趋势 | 趋势图 + 关键指标(T1-018)✓ |
一条实用规则:
失败的通知里,第一行就要让人知道"要不要现在处理" ✓
例:"主流程用例失败(影响发布)"vs"边缘用例失败(可次日处理)"✓ ——这与 《日志框架与日志规范》 的日志级别判断一致 ✓
Failures: 1
Expected: 422 Actual: 500
→ 500 说明后端异常(而不是校验失败)——先看后端日志的堆栈 ✓① CI 红:OrderApiRestAssuredTest.create_should_201 失败
② 复现:本地单跑 → 通过 ⚠️ → 分类:flaky 嫌疑(⑤)
③ 对照 5.4 的根因表:最近改动了"并发创建订单" → **本地是单线程,CI 是并行** ✓
④ 修:测试数据加 UUID 后缀([T2-006](./T2-006))✓
⑤ 防复发:把这个坑写进团队的"接口测试规范"([T2-001](./T2-001) 综合任务)✓✓问题:不知道断言的是什么 ✗ ——排查要人肉翻代码 ✗
修:断言要带描述:
assertThat(actual).as("订单 %s 的金额应按 9 折计算", orderNo).isEqualTo(expected);★ JUnit 5 / AssertJ 都支持"描述" ——失败报告立刻可读 ✓
本项目的报告(target/surefire-reports/):
cn.kb.learn.web.OrderApiRestAssuredTest.txt
cn.kb.learn.web.OrderQueryIntegrationTest.txt
...内容(成功时):
Test set: cn.kb.learn.web.OrderApiRestAssuredTest
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 21.85 s失败时的报告会包含:
java.lang.AssertionError:
Expecting...
but was...
at cn.kb.learn.web.XxxTest.method(XxxTest.java:42) ← 精确到行号 ✓★ 记住这个路径:失败时,target/surefire-reports/ 里有比控制台更完整的信息 ✓(控制台可能被截断,文件里不会)✓
进阶工具:
| 工具 | 价值 |
|---|---|
| Allure | 美观报告 + 步骤/附件/历史趋势 ✓ |
| JUnit Platform 报告(5.x) | 内置 XML/HTML ✓ |
Failures 与 Errors:故意造两类失败,观察报告里的区别 ✓;Time elapsed 的分布:哪个测试最慢?(慢测试是 CI 的债务,T1-017)✓;.as() 描述:故意失败,观察报告可读性的变化 ✓;| # | 错误 | 说明 |
|---|---|---|
| 1 | "重跑就绿"当手段 ✗✗✗ | 杀死测试体系(5.4)✗✗✗ |
| 2 | 断言无描述 ✗ | 失败后排查靠猜 ✗ |
| 3 | 只在控制台看结果 | surefire 文件信息更全 ⚠️ |
| 4 | 失败不分类,全员修产品 ⚠️ | ③④⑤类修产品是浪费 ✗ |
| 5 | flaky 无限期搁置 | 隔离区要挂牌限期修 ✓ |
| 6 | 只报告"挂了",不说"影响什么" | 受众无法决策(5.6)✗ |
目标:亲手经历"失败 → 分类 → 修复 → 防复发"。
步骤:
throw new RuntimeException → 跑 → 读报告(Errors)✓ ——对比两者的报告差异 ★;.as() 描述 → 再失败 → 观察可读性 ✓;System.currentTimeMillis() % 2 == 0"的测试 → 多跑几次 → 体验时红时绿 ✗ ——然后想"怎么修"(注入时钟 ✓);扩展任务:设计一个"flaky 治理流程":
Failures: 1 说明代码抛了异常。(对 / 错)assertThat(x).isEqualTo(y) 失败报告信息有限,怎么增强?Clock 而不是 LocalDateTime.now()——消除时序 flaky ✓。一个类比:狼来了的故事 ——警报响得太多,狼真来了没人信 ✗✗✗
.as("上下文说明") / 参数化 name / 失败时打印请求与响应(log().ifValidationFails(),T2-005)✓Clock 接口注入(生产用 Clock.systemDefaultZone(),测试用固定的 Clock.fixed(...))✓ 原理:把"不确定"变成"确定" ——flaky 的根因常常就是"不确定"(时间、随机、并发、外部)✓Failures 与 Errors 并采取不同排查方向 ★;.as() + 上下文)✓;接口测试五节(T2-001/005/006/007/008)完成。TB 的后续:
暂无