主题
界面
主题
《测试流水线》讲了流水线的结构(分几层、失败即停);本节讲流水线的产物:
流水线跑完了 —— 然后呢?
"它红了" → 然后呢?
"它绿了" → 你怎么知道"绿"是因为真的没问题,而不是因为"什么都没跑"?本节要解决的是:让"跑过"这件事留下可归档、可追溯、可对比的证据 ✓
if: always() / 保留期 / 命名 / 敏感信息)★★★;先问一个看似多余、实则致命的问题:
流水线显示"绿色通过"。
问:它到底跑了多少个测试?
答:……不知道。"绿"可能是以下几种情况 ✗:
| 情况 | 表现 | 真相 |
|---|---|---|
| 真的都通过了 ✓ | 绿 | 好 ✓ |
| 一个测试都没跑 ✗✗ | 绿 | 测试类没被匹配到(命名不符合 *Test)✗ |
| 测试被跳过了 ✗ | 绿 | @Disabled / assumeTrue 全部跳过 ✗ |
| 跑的是旧产物 ✗ | 绿 | 编译没更新 ✗(T2-011 里真实遇到过) |
| 断言没生效 ✗ | 绿 | 断言写错(如 assertTrue(true))✗ |
所以"绿色"本身不是证据,"跑了 N 个测试、N 个通过"才是证据 ✓✓
而这句话只能从报告里得到 ✓
| 层级 | 内容 | 谁看 | 存多久 |
|---|---|---|---|
| ① 原始报告 ★★ | 每个测试类的 XML(含每个方法的结果与耗时) | 排查失败的人 | 30 天 |
| ② 汇总 ★ | 总数/通过/失败/跳过/总耗时 | 所有人(合并前扫一眼) | 与构建同寿命 |
| ③ 趋势 ★★ | 历次运行的曲线 | 技术负责人 | 长期 |
★ 三者缺一不可:
只有②:失败了无法定位(不知道哪个方法挂了)✗
只有①:没人看得懂(要下载 XML 才能知道红没红)✗
没有③:看不出"性能或稳定性在缓慢劣化" ✗✗
Maven surefire 的 XML 报告(target/surefire-reports/TEST-<类名>.xml):
<testsuite name="cn.kb.learn.web.controller.OrderControllerSliceTest"
tests="7" errors="0" skipped="0" failures="0"
time="2.436" timestamp="2026-09-22T11:30:00">
<testcase name="should_return_201_with_location_when_create_succeeds"
classname="cn.kb.learn.web.controller.OrderControllerSliceTest"
time="0.31"/>
<testcase name="should_return_422_when_validation_fails"
classname="cn.kb.learn.web.controller.OrderControllerSliceTest"
time="0.052"/>
</testsuite>五个关键字段(这张表是本节的核心):
| 字段 | 含义 | 怎么用 |
|---|---|---|
tests ★★★ | 跑了多少个 | "绿是真绿吗"的自证 ✓ |
failures ★★★ | 断言失败数 | 业务缺陷 ✓ |
errors ★★ | 异常数 | 环境/配置问题(如容器起不来)✓ |
skipped ★★ | 跳过数 | ⚠️ 最容易被忽略的字段 |
time ★ | 该类总耗时 | 找"慢测试" ✓ |
★ skipped 为什么危险:
流水线绿了,但 skipped = 40 / 50 ← 80% 的测试被跳过了 ✗✗跳过的常见原因:@Disabled、assumeTrue 不成立(本项目的 UI 测试就是这样:应用没启动就整体跳过)✓
所以:"通过数 + 跳过数"必须一起看 ✓✓
★ failures 与 errors 的区别:
| 含义 | 排查方向 | |
|---|---|---|
| failures ★ | 断言不成立(业务逻辑错) | 看代码 ✓ |
| errors ★ | 抛了未预期的异常 | 看环境/配置/依赖 ✓ |
这条区分能省下大量时间 ✓✓ ——"断言失败"和"容器起不来"是完全不同的问题 ✓
归档报告不是"把文件传上去"那么简单,有四件事必须做对:
| # | 要点 | 不做的后果 |
|---|---|---|
| ① | if: always() ★★★ | 失败时报告不上传 —— 而失败时最需要它 ✗✗ |
| ② | 保留期(retention-days)★ | 报告无限堆积,占满存储 ✗ |
| ③ | 命名带上下(分支/提交号)★★ | 分不清是哪次构建的报告 ✗ |
| ④ | 注意敏感信息 ★★ | 报告里可能含 token、密码、完整请求体 ⚠️ |
① 是最容易漏的一条 ✓ ——因为 CI 的默认行为是"失败即停" ✓(T2-004 5.4)
- name: 归档测试报告
if: always() # ★ 不加这行,失败时报告就没了
uses: actions/upload-artifact@v4
with:
name: test-reports-${{ github.sha }}
path: 'labs/**/target/surefire-reports/*'
retention-days: 30④ 值得单独说:
surefire 的 XML 里会带上失败时的异常消息,而异常消息里可能含有请求体、SQL、连接串 ⚠️
UI 测试的截图里可能有真实用户数据 ⚠️
所以:对外分享报告前,先确认里面没有敏感信息 ✓
同一套分层,两种执行语义——它们对报告的影响不同:
| 失败即停 | 全量收集 | |
|---|---|---|
| 命令 | pipeline.ps1 | pipeline.ps1 -NoFailFast |
| 适用 | PR 门禁(要快给结论)✓ | 夜间跑 / 排查阶段 ✓ |
| 报告完整性 | 只有失败层之前的数据 ✗ | 所有层的数据 ✓ |
| 本轮的实测 | L2 失败 → L3 未执行,省 80.6 秒 ✓ | 会多花 80.6 秒,但能看到 L3 的状态 |
★ 这条对"报告归档"的影响很实际:
"失败即停"时,未执行的层在报告里是"空白"而不是"失败" ✓
所以看报告的人必须知道:空白 ≠ 通过 ✗✗ ——它可能是"根本没跑" ✓
这正好呼应 5.1 的层级②:汇总报告必须包含"哪些层执行了" ✓
本项目的流水线就是这么做的:
阶段 耗时(秒) 结果
L1 登记表与结构校验 3.4 通过
L2 快速单元测试(纯 Java 模块) 65.9 通过
L3 慢测试(Spring 模块:切片 + 集成) 80.6 通过
合计 149.9★ "没出现在表里的层" 就是没跑的层 ✓ ——一眼能看出来 ✓
拿到一份红报告,第一步不是看代码,而是"分类":
| 类别 | 报告里的特征 | 排查起点 |
|---|---|---|
| ① 编译失败 ★ | COMPILATION ERROR,没有 tests= 字段 | 看第一个报错的 .java 文件与行号 |
| ② 断言失败 ★ | failures=1,有 testcase 报错详情 | 看断言消息与期望值 |
| ③ 环境/配置失败 ★★ | errors=N,异常是 BeanCreationException、Connection refused | 看环境与配置,不是看业务代码 |
★ 本轮的三个真实案例(都属于①,但根因完全不同):
| 现象 | 根因 | 教训 |
|---|---|---|
Non-resolvable parent POM | <parent> 的 artifactId 写错 | 父坐标写错,Maven 无法解析模块 ✗ |
COMPILATION ERROR(位置诡异:[7,37]) | javadoc 里的 scripts\ui-test.cmd ——\u 被当作 Unicode 转义 ✗✗ | Java 的 \u 处理在注释之前(见下)✓ |
直到重跑才发现是旧产物 | 增量编译没检测到改动 | 怀疑"改了没生效"时先 clean ✓ |
★ 第二条值得单独记住:
// ✗ 错误:javadoc 里写 Windows 路径
* scripts\ui-test.cmdJava 编译器在"词法分析之前"先做 Unicode 转义处理,且这一步会扫描整个文件(含注释) ✓✓ 于是 \ui 被当作 \u 转义的开始,而 i 不是合法十六进制 → 报"在 Unicode 转义中字符无效" ✗
报错位置还会跑到"毫不相关的行"(如某个 import 的第 37 列)✗ ——让排查极其困难 ✓
正确做法:注释里的路径用正斜杠 /,或写成 \\ ✓✓
这条教训比它看起来重要:"报错位置与真实原因不在同一处"是编译期最耗时的一类问题 ✓
单次报告回答"这次行不行";趋势回答"我们的质量在往哪走" ✓✓
三个值得长期看的趋势:
| 趋势 | 含义 | 危险信号 |
|---|---|---|
| 测试总数 ★★ | 测试在增加吗 | 总数长期不动,而代码量在涨 → 测试没跟上 ✗ |
| 跳过数 ★★★ | 有多少测试没真跑 | 跳过数持续增长 → 有人用 @Disabled 消音 ✗✗ |
| 总耗时 ★★ | 反馈速度 | 持续增长到 30 分钟以上 → 没人等了 ✗(T2-004 5.6①) |
★ 其中"跳过数"是最值得盯的:
@Disabled("暂时跳过,下周修")是技术债里最容易被遗忘的一种 ✓✓因为它不会报错、不会变红、只在报告里多一个数字 ✓
本项目在自己身上也做过这件事(T2-014):UI 测试在"应用未启动"时整体跳过, 报告里是"绿"的 ✗ ——所以 ui-test.ps1 特意把"跑了多少个"打印出来 ✓
# 统计所有模块的测试数与跳过数
Get-ChildItem -Path labs/*/target/surefire-reports/TEST-*.xml | ForEach-Object {
[xml]$x = Get-Content $_ -Encoding utf8
"{0,-50} tests={1} failures={2} errors={3} skipped={4}" -f `
$x.testsuite.name, $x.testsuite.tests, $x.testsuite.failures, `
$x.testsuite.errors, $x.testsuite.skipped
}流水线通过 ✓
本次执行:116 个测试,0 失败,0 错误,0 跳过
分层:L1 通过 / L2 通过 / L3 通过★ 关键在最后三个数字:
"0 跳过" = 所有测试都真的跑了 ✓✓
没有这三个数字,"通过"是没有说服力的 ✗
✓ pipeline passed (3m 12s) ← 就这一行读者无法判断:跑了什么、跑了多少、有没有跳过 ✗✗
统计命令(见 6.1)在本项目的输出**(部分)**:
cn.kb.learn.web.controller.OrderControllerSliceTest tests=7 failures=0 errors=0 skipped=0
cn.kb.learn.web.OrderApiIntegrationTest tests=4 failures=0 errors=0 skipped=0
cn.kb.learn.web.OrderQueryIntegrationTest tests=2 failures=0 errors=0 skipped=0
cn.kb.learn.ui.UiSmokeTest tests=6 failures=0 errors=0 skipped=0★ 从这份报告能读出三件事:
| 读出的信息 | 依据 |
|---|---|
| 切片测试比集成测试的用例多 | 7 vs 4(T1-016 ✓) |
| UI 测试确实执行了 | tests=6 ✓ ——而不是被排除后"绿" ✓ |
| 没有跳过 | skipped=0 ✓ |
★ 第三条尤其重要:
ui-testing/pom.xml里默认excludedGroups=ui——也就是说,mvn test时 UI 测试是被排除的 ✗如果看报告时不看
tests数字,会误以为"UI 测试通过了",其实是"一个都没跑" ✗✗所以
ui-test.ps1用-DexcludedGroups=显式覆盖它,并在结束时打印 "Tests run" ✓
这正是 5.1 那句"绿色不是证据"的实例 ✓✓
★ 写这一节时,我自己正好踩了同一个坑(真实记录,原文照抄):
[INFO] Results:
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS流水线绿了,但一个用例都没跑 ✗✗
经过:脚本用 -DexcludedGroups=(空值)去覆盖 pom 里的 <excludedGroups>ui</excludedGroups>, 而 Maven 会忽略空值属性 ✗ ——pom 里的排除依然生效 ✓ ——于是所有 UI 用例都被过滤掉, surefire 报 0 个用例,构建依然成功 ✗
改法:不要用空值覆盖属性,改用 profile 切换 ✓✓
<!-- 默认 profile:排除 ui -->
<profile>
<id>default-ui-excluded</id>
<activation><activeByDefault>true</activeByDefault></activation>
<build><plugins><plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration><excludedGroups>ui</excludedGroups></configuration>
</plugin></plugins></build>
</profile>
<!-- 显式激活它时,默认 profile 自动失效,于是不再排除,只保留 groups=ui -->
<profile>
<id>ui-tests</id>
<build><plugins><plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration><groups>ui</groups></configuration>
</plugin></plugins></build>
</profile>mvn test -pl ui-testing -Pui-tests # ✓ 正确:切换 profile
mvn test -pl ui-testing -DexcludedGroups= # ✗ 错误:空值被忽略,仍排除 ui这条案例同时印证了两件事 ✓:
tests 数自证(5.1)✓ ——"Tests run: 0" 就是"没跑" ✓TEST-*.xml 的 tests 总和 → 与预期对比(本项目 ≈ 116+)★;skipped > 0 的报告 → 看为什么跳过(@Disabled?assumeTrue?)★★;failures 与 errors:前者看代码,后者看环境 ✓;if: always() 去掉,然后故意让测试失败 → 观察报告是否还在 ★★;| # | 错误 | 说明 |
|---|---|---|
| 1 | 只看红绿,不看 tests 数 ✗✗ | "绿"可能是"没跑"(第 4 节)✗✗ |
| 2 | 归档忘了 if: always() ✗✗ | 失败时报告丢失(5.3①)✗✗ |
| 3 | 不看 skipped ✗✗ | 跳过数增长 = 测试在悄悄消失 ✗ |
| 4 | failures 与 errors 不分 ⚠️ | 浪费时间在错误的方向 ✗ |
| 5 | 报告里带敏感信息就对外分享 ⚠️ | 泄露(5.3④)✗ |
| 6 | 报告无限保留 | 占满存储 ✗ |
| 7 | "改了没生效"时不 clean ⚠️ | 增量编译的坑(本轮的案例之一)✓ |
目标:让流水线的"绿"变得可自证。
步骤:
tests 总和,记下来 ✓;skipped,如果有,找出原因 ★;failures=1 与报错详情 ✓;@Disabled)→ 观察 skipped=1,而流水线仍然是"绿" ★★;pipeline.ps1 加"汇总输出":把各模块的 tests/failures/errors/skipped 汇总成一行 ✓。验收方式:能说出"你这次流水线跑了多少个测试、有多少跳过"。
扩展任务:把报告变成趋势数据:
failures 表示 ______ 类问题,errors 表示 ______ 类问题,______ 字段最容易被人忽略但最能反映"测试在消失"。if: always()?tests=120 failures=0 errors=0 skipped=95,该怎么解读?skipped > 0 时告警。tests 与 skipped ✓(第 4 节)skipped ✓@Disabled 或环境不满足的 assumeTrue)✓tests 自证 ★★★;if: always(),并知道为什么 ★★★;failures(看代码)与 errors(看环境) ★★;skipped 增长是"测试在悄悄消失"的信号 ★★★;\u 在 Java 注释里也会被当成转义(5.5)★★。《测试环境与数据治理》:让"跑得通"变成"每次都能跑通"。
暂无