Skip to content

CI 分层执行与报告归档 ​

T2-017必学高级约 60 分钟JDK 21测试专项 B:接口·UI·性能 · 测试流水线

1. 本节定位 ​

《测试流水线》讲了流水线的结构(分几层、失败即停);本节讲流水线的产物:

text
流水线跑完了 —— 然后呢?
   "它红了"  →  然后呢?
   "它绿了"  →  你怎么知道"绿"是因为真的没问题,而不是因为"什么都没跑"?

本节要解决的是:让"跑过"这件事留下可归档、可追溯、可对比的证据 ✓

2. 学习目标 ​

  • 说出测试报告的三个层级(原始 / 汇总 / 趋势)★★;
  • 会读 surefire 的 XML 报告,知道关键字段 ★★★;
  • 知道归档报告的四个要点(if: always() / 保留期 / 命名 / 敏感信息)★★★;
  • 能回答"绿是真绿吗"(用"跑了多少个测试"自证)★★★;
  • 会从报告定位三类失败(编译 / 断言 / 环境)★★★;
  • 知道"历史趋势"怎么把报告变成退化预警 ★★。

3. 前置知识检查 ​

  • 完成 T2-004 的分层实测了吗?✓
  • 知道 surefire 报告在哪吗?(T1-016)✓

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

先问一个看似多余、实则致命的问题:

text
流水线显示"绿色通过"。
问:它到底跑了多少个测试?
答:……不知道。

"绿"可能是以下几种情况 ✗:

情况表现真相
真的都通过了 ✓绿好 ✓
一个测试都没跑 ✗✗绿测试类没被匹配到(命名不符合 *Test)✗
测试被跳过了 ✗绿@Disabled / assumeTrue 全部跳过 ✗
跑的是旧产物 ✗绿编译没更新 ✗(T2-011 里真实遇到过)
断言没生效 ✗绿断言写错(如 assertTrue(true))✗

所以"绿色"本身不是证据,"跑了 N 个测试、N 个通过"才是证据 ✓✓

而这句话只能从报告里得到 ✓

5. 核心概念 ​

5.1 报告的三个层级 ★★ ​

层级内容谁看存多久
① 原始报告 ★★每个测试类的 XML(含每个方法的结果与耗时)排查失败的人30 天
② 汇总 ★总数/通过/失败/跳过/总耗时所有人(合并前扫一眼)与构建同寿命
③ 趋势 ★★历次运行的曲线技术负责人长期

★ 三者缺一不可:

只有②:失败了无法定位(不知道哪个方法挂了)✗

只有①:没人看得懂(要下载 XML 才能知道红没红)✗

没有③:看不出"性能或稳定性在缓慢劣化" ✗✗

5.2 读 surefire 报告:字段就是答案 ★★★ ​

Maven surefire 的 XML 报告(target/surefire-reports/TEST-<类名>.xml):

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 为什么危险:

text
流水线绿了,但 skipped = 40 / 50     ← 80% 的测试被跳过了 ✗✗

跳过的常见原因:@Disabled、assumeTrue 不成立(本项目的 UI 测试就是这样:应用没启动就整体跳过)✓

所以:"通过数 + 跳过数"必须一起看 ✓✓

★ failures 与 errors 的区别:

含义排查方向
failures ★断言不成立(业务逻辑错)看代码 ✓
errors ★抛了未预期的异常看环境/配置/依赖 ✓

这条区分能省下大量时间 ✓✓ ——"断言失败"和"容器起不来"是完全不同的问题 ✓

5.3 归档的四个要点 ★★★ ​

归档报告不是"把文件传上去"那么简单,有四件事必须做对:

#要点不做的后果
①if: always() ★★★失败时报告不上传 —— 而失败时最需要它 ✗✗
②保留期(retention-days)★报告无限堆积,占满存储 ✗
③命名带上下(分支/提交号)★★分不清是哪次构建的报告 ✗
④注意敏感信息 ★★报告里可能含 token、密码、完整请求体 ⚠️

① 是最容易漏的一条 ✓ ——因为 CI 的默认行为是"失败即停" ✓(T2-004 5.4)

yaml
- 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 测试的截图里可能有真实用户数据 ⚠️

所以:对外分享报告前,先确认里面没有敏感信息 ✓

5.4 分层执行的两种模式 ★★ ​

同一套分层,两种执行语义——它们对报告的影响不同:

失败即停全量收集
命令pipeline.ps1pipeline.ps1 -NoFailFast
适用PR 门禁(要快给结论)✓夜间跑 / 排查阶段 ✓
报告完整性只有失败层之前的数据 ✗所有层的数据 ✓
本轮的实测L2 失败 → L3 未执行,省 80.6 秒 ✓会多花 80.6 秒,但能看到 L3 的状态

★ 这条对"报告归档"的影响很实际:

"失败即停"时,未执行的层在报告里是"空白"而不是"失败" ✓

所以看报告的人必须知道:空白 ≠ 通过 ✗✗ ——它可能是"根本没跑" ✓

这正好呼应 5.1 的层级②:汇总报告必须包含"哪些层执行了" ✓

本项目的流水线就是这么做的:

text
阶段                                              耗时(秒)       结果
L1 登记表与结构校验                                       3.4       通过
L2 快速单元测试(纯 Java 模块)                            65.9       通过
L3 慢测试(Spring 模块:切片 + 集成)                       80.6       通过
合计                                                    149.9

★ "没出现在表里的层" 就是没跑的层 ✓ ——一眼能看出来 ✓

5.5 从报告定位三类失败 ★★★ ​

拿到一份红报告,第一步不是看代码,而是"分类":

类别报告里的特征排查起点
① 编译失败 ★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 ✓

★ 第二条值得单独记住:

java
// ✗ 错误:javadoc 里写 Windows 路径
 *   scripts\ui-test.cmd

Java 编译器在"词法分析之前"先做 Unicode 转义处理,且这一步会扫描整个文件(含注释) ✓✓ 于是 \ui 被当作 \u 转义的开始,而 i 不是合法十六进制 → 报"在 Unicode 转义中字符无效" ✗

报错位置还会跑到"毫不相关的行"(如某个 import 的第 37 列)✗ ——让排查极其困难 ✓

正确做法:注释里的路径用正斜杠 /,或写成 \\ ✓✓

这条教训比它看起来重要:"报错位置与真实原因不在同一处"是编译期最耗时的一类问题 ✓

5.6 把报告变成趋势 ★★ ​

单次报告回答"这次行不行";趋势回答"我们的质量在往哪走" ✓✓

三个值得长期看的趋势:

趋势含义危险信号
测试总数 ★★测试在增加吗总数长期不动,而代码量在涨 → 测试没跟上 ✗
跳过数 ★★★有多少测试没真跑跳过数持续增长 → 有人用 @Disabled 消音 ✗✗
总耗时 ★★反馈速度持续增长到 30 分钟以上 → 没人等了 ✗(T2-004 5.6①)

★ 其中"跳过数"是最值得盯的:

@Disabled("暂时跳过,下周修") 是技术债里最容易被遗忘的一种 ✓✓

因为它不会报错、不会变红、只在报告里多一个数字 ✓

本项目在自己身上也做过这件事(T2-014):UI 测试在"应用未启动"时整体跳过, 报告里是"绿"的 ✗ ——所以 ui-test.ps1 特意把"跑了多少个"打印出来 ✓

6. 递进示例 ​

6.1 最小示例:从报告里读出"跑了多少" ​

powershell
# 统计所有模块的测试数与跳过数
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
}

6.2 业务示例:一条"自证"的汇总 ​

text
流水线通过 ✓
本次执行:116 个测试,0 失败,0 错误,0 跳过
分层:L1 通过 / L2 通过 / L3 通过

★ 关键在最后三个数字:

"0 跳过" = 所有测试都真的跑了 ✓✓

没有这三个数字,"通过"是没有说服力的 ✗

6.3 反例:只有红绿灯 ​

text
✓ pipeline passed (3m 12s)      ← 就这一行

读者无法判断:跑了什么、跑了多少、有没有跳过 ✗✗

7. 实测:本项目报告的真实结构 ​

统计命令(见 6.1)在本项目的输出**(部分)**:

text
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 那句"绿色不是证据"的实例 ✓✓

★ 写这一节时,我自己正好踩了同一个坑(真实记录,原文照抄):

text
[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 切换 ✓✓

xml
<!-- 默认 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>
powershell
mvn test -pl ui-testing -Pui-tests     # ✓ 正确:切换 profile
mvn test -pl ui-testing -DexcludedGroups=   # ✗ 错误:空值被忽略,仍排除 ui

这条案例同时印证了两件事 ✓:

  1. "绿"必须用 tests 数自证(5.1)✓ ——"Tests run: 0" 就是"没跑" ✓
  2. "没跑"与"跳过"都不是"通过"(T2-018 5.6)✓

8. Debug 观察点 ​

  1. 统计所有 TEST-*.xml 的 tests 总和 → 与预期对比(本项目 ≈ 116+)★;
  2. 找 skipped > 0 的报告 → 看为什么跳过(@Disabled?assumeTrue?)★★;
  3. 对比 failures 与 errors:前者看代码,后者看环境 ✓;
  4. 把 if: always() 去掉,然后故意让测试失败 → 观察报告是否还在 ★★;
  5. 给报告加上分支与提交号,跑两次不同分支 → 观察能否区分 ✓。

9. 常见错误 ​

#错误说明
1只看红绿,不看 tests 数 ✗✗"绿"可能是"没跑"(第 4 节)✗✗
2归档忘了 if: always() ✗✗失败时报告丢失(5.3①)✗✗
3不看 skipped ✗✗跳过数增长 = 测试在悄悄消失 ✗
4failures 与 errors 不分 ⚠️浪费时间在错误的方向 ✗
5报告里带敏感信息就对外分享 ⚠️泄露(5.3④)✗
6报告无限保留占满存储 ✗
7"改了没生效"时不 clean ⚠️增量编译的坑(本轮的案例之一)✓

10. 动手实验 ​

目标:让流水线的"绿"变得可自证。

步骤:

  1. 跑一次流水线,统计 tests 总和,记下来 ✓;
  2. 看有没有 skipped,如果有,找出原因 ★;
  3. 故意让一个断言失败(改一个期望值),跑一次 → 观察报告里 failures=1 与报错详情 ✓;
  4. 故意让一个测试跳过(加 @Disabled)→ 观察 skipped=1,而流水线仍然是"绿" ★★;
  5. 给 pipeline.ps1 加"汇总输出":把各模块的 tests/failures/errors/skipped 汇总成一行 ✓。

验收方式:能说出"你这次流水线跑了多少个测试、有多少跳过"。

扩展任务:把报告变成趋势数据:

  • 每次运行把汇总写进一个 JSONL(追加一行)✓;
  • 对比最近三次,输出"测试数变化 / 跳过数变化 / 耗时变化" ★;
  • 说明:这就是"最小可用的质量趋势看板" ✓

11. 练习题 ​

  1. 判断:流水线显示绿色,说明测试全部通过。(对 / 错)
  2. 填空:报告里 failures 表示 ______ 类问题,errors 表示 ______ 类问题,______ 字段最容易被人忽略但最能反映"测试在消失"。
  3. 简答:为什么归档报告必须加 if: always()?
  4. (代码阅读)某报告:tests=120 failures=0 errors=0 skipped=95,该怎么解读?
  5. (编程)写一个脚本:遍历所有 surefire 报告,输出"总数/失败/错误/跳过/总耗时",并在 skipped > 0 时告警。
参考答案(要点)
  1. 错。"绿"可能意味着"没跑"或"全跳过" ✓ ——必须看 tests 与 skipped ✓(第 4 节)
  2. 断言不成立;异常(环境/配置);skipped ✓
  3. 因为 CI 默认"失败即停",而失败时最需要报告 ✓ ——不加就形成"越需要越没有"的悖论 ✓★
  4. 120 个测试里 95 个被跳过 ✗ ——实际只跑了 25 个 ✓ ——"绿"的含金量很低 ✗ ——要查为什么跳过(大概率是 @Disabled 或环境不满足的 assumeTrue)✓
  5. 要点:遍历 XML + 解析字段 + 累加 + 阈值判断 ✓ ——本质上就是 CI 汇总面板做的事 ✓

12. 掌握标准 ​

  • [ ] 能说出报告的三个层级(原始/汇总/趋势) ★★;
  • [ ] 会读 surefire XML 的五个关键字段 ★★★;
  • [ ] 知道"绿 ≠ 跑了",并能用 tests 自证 ★★★;
  • [ ] 知道归档必须 if: always(),并知道为什么 ★★★;
  • [ ] 能区分 failures(看代码)与 errors(看环境) ★★;
  • [ ] 知道 skipped 增长是"测试在悄悄消失"的信号 ★★★;
  • [ ] 知道 \u 在 Java 注释里也会被当成转义(5.5)★★。

13. 扩展资源 ​

14. 下一步学习 ​

《测试环境与数据治理》:让"跑得通"变成"每次都能跑通"。

后续知识

暂无

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

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