Skip to content

测试流水线 ​

T2-004建议学高级约 90 分钟JDK 21测试专项 B:接口·UI·性能 · 测试流水线

1. 本节定位 ​

TA 单元测试与 TB 接口测试解决了"怎么测";从本节起解决**"怎么让测试真正跑起来"**:

text
测试用例躺在仓库里,不会自己运行。                ← 问题在这里
谁来跑?什么时候跑?失败了谁负责?跑多久?

流水线的本质:把"该跑什么、什么顺序、失败怎么办"写成可执行的代码 ✓

2. 学习目标 ​

  • 说出没有流水线时测试会怎样退化(三个必然结果)★★★;
  • 说出分层的依据,并知道"分层不等于分快慢" ★★★;
  • 会读会写 GitHub Actions 的 YAML 结构 ★;
  • 知道"失败即停"省下的是什么(本轮实测:省 80.6 秒)★★;
  • 知道分层的收益取决于什么(本轮实测的反直觉发现)★★★;
  • 说出流水线的四个常见陷阱与对策 ✓。

3. 前置知识检查 ​

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

先说清楚"没有流水线会怎样" ——这不是理论,是三条必然规律:

#现象原因
①测试会被跳过 ✗本地跑全量要几分钟 → "我先提交,回头再跑" → 永远没回头
②执行不一致 ✗有人跑全量、有人只跑自己改的模块、有人用 IDE 跑 → 结论不可比
③环境不一致 ✗✗"在我机器上是好的" → 因为别人机器上缺一步手工操作

所以流水线要解决的是:让"测试跑过"变成一件客观的、可复现的、不依赖人的事情 ✓✓

先看一个真实的流水线输出(本节的实测数据):

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

这张表就是流水线的产出 ✓ ——它回答三个问题:跑了什么、花了多久、过没过 ✓

5. 核心概念 ​

5.1 分层:不是"分快慢",而是"分失败的可能原因" ★★★ ​

新手常见的设计是把测试按"快/慢"分成两堆。但更有用的划分依据是:

每一层失败时,你会去做什么? ✓✓

层失败意味着你该做的事典型耗时
L1 静态检查 ★格式/引用/登记不一致改一下就行秒级
L2 单元测试 ★某个函数逻辑错了打开那个类十几秒~1 分钟
L3 集成/切片测试 ★★组件之间接错了看配置、看容器启动1~3 分钟
L4 端到端/UI ★★业务流程断了看截图、看接口日志5~30 分钟
L5 性能性能退化了看拐点小时级

★ 这个划分的实操价值:

每层的"失败信号"指向不同的排查方向 ✓

如果层与层之间"失败原因重叠",那层就该合并 ✗ ——它只是拖长了时间 ✓

5.2 本轮的实测:一张分层的成绩单 ★★★ ​

实测环境:本项目 labs(17 个 Maven 模块),脚本 scripts/pipeline.ps1。

text
L1 登记表与结构校验              3.4 秒    ← 秒级,先跑
L2 快速单元测试(11 个纯 Java 模块)  65.9 秒
L3 慢测试(4 个 Spring 模块)       80.6 秒
合计                          149.9 秒   ≈ 2.5 分钟

★ 一个反直觉的发现:

L2(11 个模块)只比 L3(4 个模块)快 15 秒 ✗

原因:L2 的时间几乎全花在"编译"上,而不是"跑测试" ✓✓

怎么验证这个判断:看 surefire 报告的"Tests run"总耗时(T1-016)—— 测试本身只占一小部分,Maven 的构建流程才是大头 ✓

★ 这个发现改变了什么:

直觉实际
"纯 Java 模块快,所以先跑"11 个模块的编译开销 = 4 个 Spring 模块的编译开销 ✗
"分层是为了省时间"分层的首要价值是"定位失败原因",其次才是省时间 ✓✓

所以:

如果一次全量测试只要 1 分钟,分层的意义主要是"信号清晰",而不是"省时间" ✓

只有当某一层达到"十几分钟"时,分层才带来真正的速度收益 ✓

5.3 失败即停:省下的是"确定没有意义的等待" ★★ ​

本轮的真实案例(就在写这节之前发生):

text
第一次运行:
  L1 登记表与结构校验        2.9 秒   通过
  L2 快速单元测试            19.3 秒  失败    ← 模块 ui-testing 的 pom 写错了一行
  合计                     22.2 秒

   → L3 与 L4 被跳过,一次都没跑

如果"失败也继续跑"会怎样:

text
L1 通过 → L2 失败(pom 写错) → L3 继续跑 80 秒 → 也失败 → L4 继续跑 3 分钟 → 也失败
结果:多花 4 分钟,得到的还是"pom 写错了"这一个信息 ✗✗

★ "失败即停"的逻辑:

后面的层依赖前面的层"至少能编译/能启动" ✓

前面失败了,后面必然失败 ——继续跑只是在浪费时间 ✓✓

★ 但有一个重要例外:

场景该用哪种
CI 的主流程(PR 门禁)失败即停 ✓ ——快速给结论
夜间全量跑 / 排查阶段失败也继续(-NoFailFast)✓ ——一次收集全部问题

本项目的脚本两种都支持:

powershell
pwsh -File scripts\pipeline.ps1              # 失败即停(默认,模拟 CI 门禁)
pwsh -File scripts\pipeline.ps1 -NoFailFast  # 跑完全部层,收集所有失败

5.4 CI 配置:YAML 只是"把命令写下来" ★ ​

以 GitHub Actions 为例(其余 CI 大同小异):

yaml
name: pipeline
on: [push, pull_request]

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # ★ 缓存:把 Maven 本地仓库缓存起来,否则每次都重新下载依赖
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          cache: maven

      # L1:最便宜的一层,放在最前
      - name: L1 静态校验
        run: |
          python scripts/gen_site_data.py
          python scripts/validate.py

      # L2:单元测试(主流程;失败即止,GitHub Actions 默认行为)
      - name: L2 单元测试
        working-directory: labs
        run: ./mvnw -B -ntp test

      # L3:集成测试(可以只在需要时跑)
      - name: L3 集成测试
        working-directory: labs
        run: ./mvnw -B -ntp -pl spring-boot-web test

      # ★ 始终上传报告:即使失败也要能看到细节
      - name: 归档测试报告
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: surefire-reports
          path: 'labs/**/target/surefire-reports/*'

★ 三个必须知道的细节:

细节为什么
cache: maven ★不缓存的话,每次 CI 都要下载几百 MB 依赖 ✗(CI 的主要耗时来源)
if: always() ★★失败时最需要报告 ✗ ——而默认行为是"失败就跳过后续步骤" ✓
./mvnw 而不是 mvn ★wrapper 保证 CI 与环境用同一个 Maven 版本 ✓(《Maven 基础》)

★ if: always() 是最容易漏的一条:

流水线失败时,你想看报告;而报告步骤因为"失败即停"被跳过了 ✗✗ ——这是很讽刺的悖论 ✓

5.5 本项目的流水线脚本 ★ ​

脚本:scripts/pipeline.ps1(对应 CI 配置,本地可复现)

powershell
# L1:静态校验(最快,先跑)
Invoke-Stage "L1 登记表与结构校验" {
    python -X utf8 scripts/gen_site_data.py | Out-Null
    python -X utf8 scripts/validate.py | Out-Null
}

# L2:快速单元测试(纯 Java 模块,无 Spring 上下文)
Invoke-Stage "L2 快速单元测试(纯 Java 模块)" {
    Push-Location labs
    .\mvnw.cmd -B -ntp -pl $fastModules test | Out-Null
    Pop-Location
}

★ 为什么本地要有一份脚本:

CI 的 YAML 无法在本地调试 ✗ ——而流水线的逻辑(分层、失败即停)应该在本地可验证 ✓

本地脚本与 CI 配置"同构",改一处就能同步 ✓✓

⚠️ 一个踩过的坑(写脚本时真实遇到的):

powershell
# ✗ 错误写法:PowerShell 函数会把内部命令的输出混进返回值
function Invoke-Stage {
    & $Action
    return $LASTEXITCODE          # ← $code 会变成数组,`-ne 0` 恒为真
}

后果:L1 明明通过,L2/L3 却被全部跳过 ✗ ——而日志显示"L1 通过" ✗✗(极具迷惑性)

正确写法:用 $script: 变量传出状态,不在函数里 return ✓(见脚本注释)

5.6 流水线的四个陷阱 ★★★ ​

#陷阱现象对策
①流水线时长失控 ✗✗从 3 分钟涨到 40 分钟 → 大家不看结果了设预算(如 10 分钟),超了就拆/并行/降频 ✓
②flaky 用例 ✗✗重跑一次就绿 → 大家学会"点重试"当缺陷处理(T2-011)✓
③环境漂移 ✗某天起全部失败 → 因为 CI 镜像变了锁定版本(JDK/Maven/依赖)✓
④报告没人看 ✗失败了只看"红/绿",不看具体失败归档报告 + 失败时贴关键行 ✓

★ 关于①,有个反直觉的规律:

流水线时长与"有人看结果"的比例成反比 ✓

3 分钟:大家会等结果再合并 ✓ 30 分钟:大家合并完就去干别的了,红绿灯形同虚设 ✗✗

这解释了为什么"分层 + 快速失败"如此重要:它保住了"人愿意等"这个前提 ✓✓

6. 递进示例 ​

6.1 最小示例:一条命令的流水线 ​

cmd
@echo off
cd /d "%~dp0..\labs"
call mvnw.cmd -B -ntp test || exit /b 1
echo 流水线通过

6.2 业务示例:本项目的三层流水线 ​

见 5.2 的实测数据 ✓

6.3 反例:把所有测试一次性跑完 ​

text
全量跑(含 UI 与性能)→ 45 分钟 → 大家不看结果了 ✗

7. 完整可运行代码与实测 ​

脚本:scripts/pipeline.ps1(分层 + 失败即停 + 耗时统计)

运行(建议用 pwsh,见 .codebuddy/rules/build-and-verify.md 约束 4):

powershell
pwsh -File scripts\pipeline.ps1 -SkipBuild     # 跳过站点构建,约 2.5 分钟
pwsh -File scripts\pipeline.ps1                # 含站点构建,约 5 分钟

实测输出(真实运行结果):

text
===== L1 登记表与结构校验 =====
===== L2 快速单元测试(纯 Java 模块) =====
===== L3 慢测试(Spring 模块:切片 + 集成) =====

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

L2 与 L3 的模块构成:

text
L2(11 个模块):start, oop-basics, polymorphism, java-syntax, common-classes,
                collections, functional, concurrency, version-features,
                testing-unit, testing-testng
L3(4 个模块): spring-core, spring-boot-web, data-access, web-basics

★ 注意"模块数"与"耗时"不成比例:

层模块数耗时每模块平均
L21165.9 s6.0 s
L3480.6 s20.2 s

每模块成本差 3.4 倍 ✓ ——这才是"该分层"的真实依据 ✓✓

★ 一句可直接写进报告的结论:

"Spring 模块的单模块测试成本是纯 Java 模块的 3.4 倍(20.2s vs 6.0s), 因为它们要为每个测试类启动 Spring 容器。 所以新增测试时优先放在纯 Java 模块,除非确实需要容器。" ✓✓

8. Debug 观察点 ​

  1. 故意让 L1 失败(改一个登记项)→ 观察 L2/L3 是否被跳过、总耗时降到多少 ★;
  2. 用 -NoFailFast 重跑 → 对比"一次收集全部问题"与"快速失败"的时间差 ★★;
  3. 看 Maven 的 -pl 参数:只构建指定模块,但 Maven 仍要解析全部模块的 pom ★(本轮踩过:一个模块的 pom 写错,导致所有 -pl 命令都失败)✓;
  4. 对比第一次与第二次运行的耗时:第二次更快(依赖已缓存 + 编译产物复用)✓;
  5. 统计 surefire 的"Tests run"总耗时 vs 整层耗时 → 算出"编译占多少、测试占多少" ★★。

9. 常见错误 ​

#错误说明
1流水线里没有静态检查 ✗格式/引用问题要等到运行期才暴露 ✗
2失败后继续跑完全部层 ✗✗浪费时间(5.3)✗
3报告不归档 ✗✗失败时看不到细节,只能本地复现 ✗
4忘了 if: always() ✗✗失败时报告步骤被跳过(5.4)✗✗
5不缓存依赖 ✗CI 大部分时间在下载 ✗
6流水线时长无预算 ✗✗涨到 40 分钟 → 没人看结果 ✗(5.6①)
7以为"分层 = 分快慢" ✗真正的依据是"失败原因不同" ✓(5.1)

10. 动手实验 ​

目标:亲手搭一条分层流水线,并量出每层的成本。

步骤:

  1. 用 scripts/pipeline.ps1 跑一次,记录三层的耗时 ✓;
  2. 把 L2 单独跑一次,统计它包含多少个测试类(看 surefire 报告)✓;
  3. 算出"每模块平均耗时",比较 L2 与 L3(提示:本轮是 6.0s vs 20.2s)★;
  4. 故意制造一次 L1 失败,观察"失败即停"省下了多少秒 ✓;
  5. 给自己的项目写一份 CI YAML(含缓存与 if: always())✓。

验收方式:能说出"你这三层的耗时各是多少、为什么这样分"。

扩展任务:给流水线加一层 L0(提交信息检查):

  • 校验提交信息符合 Conventional Commits(《Git 基础与 cherry-pick》);
  • 说明:为什么这一层应该放在 L1 之前(提示:它是"最便宜"的检查,且失败时给出的是"改消息"这种秒级操作)✓

11. 练习题 ​

  1. 判断:流水线分层的目的主要是为了节省总时长。(对 / 错)
  2. 填空:分层的真正依据是 ______,而"快慢"只是 ______。
  3. 简答:为什么"失败即停"能省时间?举一个本轮的真实例子。
  4. (代码阅读)某 CI 配置里报告归档写成 uses: actions/upload-artifact@v4(没有 if: always()),会有什么后果?
  5. (编程)给 pipeline.ps1 加"每层结束后输出该层的测试统计"(从 surefire 报告读 Tests run)。
参考答案(要点)
  1. 错。首要目的是"失败信号指向不同的排查方向" ✓ ——省时间是次要收益,且取决于慢层是否真的慢 ✓(5.2 的反直觉发现)
  2. 每层失败意味着去做什么;一种外在表现 ✓
  3. 因为后面的层依赖前面的层能编译/能启动 ✓ ——本轮真实例子:L2 因 ui-testing 的 pom 写错而失败,L3 被跳过,省下 80.6 秒 ✓
  4. 失败时报告不会被上传 ✗ ——而失败时恰恰最需要报告 ✓★ ——这是"失败即停"的默认行为造成的悖论 ✓
  5. 要点:在 Invoke-Stage 之后读 labs/*/target/surefire-reports/*.txt,累加 Tests run 的数字 ✓

12. 掌握标准 ​

  • [ ] 能说出没有流水线时测试会怎样退化(三条) ★★★;
  • [ ] 知道分层的依据是"失败原因不重叠",而不只是快慢 ★★★;
  • [ ] 会写含缓存与 if: always() 的 CI YAML ★;
  • [ ] 知道"失败即停"省下的是"确定无意义的等待" ★★;
  • [ ] 知道分层的速度收益取决于"慢层是否真的慢"(本轮的 6.0s vs 20.2s)★★★;
  • [ ] 能说出流水线时长失控的后果(人不再看结果)★★。

13. 扩展资源 ​

14. 下一步学习 ​

《CI 分层执行与报告归档》:把测试结果变成"可归档、可追溯"的证据。

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

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