主题
界面
主题
TA 单元测试与 TB 接口测试解决了"怎么测";从本节起解决**"怎么让测试真正跑起来"**:
测试用例躺在仓库里,不会自己运行。 ← 问题在这里
谁来跑?什么时候跑?失败了谁负责?跑多久?流水线的本质:把"该跑什么、什么顺序、失败怎么办"写成可执行的代码 ✓
先说清楚"没有流水线会怎样" ——这不是理论,是三条必然规律:
| # | 现象 | 原因 |
|---|---|---|
| ① | 测试会被跳过 ✗ | 本地跑全量要几分钟 → "我先提交,回头再跑" → 永远没回头 |
| ② | 执行不一致 ✗ | 有人跑全量、有人只跑自己改的模块、有人用 IDE 跑 → 结论不可比 |
| ③ | 环境不一致 ✗✗ | "在我机器上是好的" → 因为别人机器上缺一步手工操作 |
所以流水线要解决的是:让"测试跑过"变成一件客观的、可复现的、不依赖人的事情 ✓✓
先看一个真实的流水线输出(本节的实测数据):
===== 流水线结果 =====
阶段 耗时(秒) 结果
L1 登记表与结构校验 3.4 通过
L2 快速单元测试(纯 Java 模块) 65.9 通过
L3 慢测试(Spring 模块:切片 + 集成) 80.6 通过
合计 149.9这张表就是流水线的产出 ✓ ——它回答三个问题:跑了什么、花了多久、过没过 ✓
新手常见的设计是把测试按"快/慢"分成两堆。但更有用的划分依据是:
每一层失败时,你会去做什么? ✓✓
| 层 | 失败意味着 | 你该做的事 | 典型耗时 |
|---|---|---|---|
| L1 静态检查 ★ | 格式/引用/登记不一致 | 改一下就行 | 秒级 |
| L2 单元测试 ★ | 某个函数逻辑错了 | 打开那个类 | 十几秒~1 分钟 |
| L3 集成/切片测试 ★★ | 组件之间接错了 | 看配置、看容器启动 | 1~3 分钟 |
| L4 端到端/UI ★★ | 业务流程断了 | 看截图、看接口日志 | 5~30 分钟 |
| L5 性能 | 性能退化了 | 看拐点 | 小时级 |
★ 这个划分的实操价值:
每层的"失败信号"指向不同的排查方向 ✓
如果层与层之间"失败原因重叠",那层就该合并 ✗ ——它只是拖长了时间 ✓
实测环境:本项目 labs(17 个 Maven 模块),脚本 scripts/pipeline.ps1。
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 分钟,分层的意义主要是"信号清晰",而不是"省时间" ✓
只有当某一层达到"十几分钟"时,分层才带来真正的速度收益 ✓
本轮的真实案例(就在写这节之前发生):
第一次运行:
L1 登记表与结构校验 2.9 秒 通过
L2 快速单元测试 19.3 秒 失败 ← 模块 ui-testing 的 pom 写错了一行
合计 22.2 秒
→ L3 与 L4 被跳过,一次都没跑如果"失败也继续跑"会怎样:
L1 通过 → L2 失败(pom 写错) → L3 继续跑 80 秒 → 也失败 → L4 继续跑 3 分钟 → 也失败
结果:多花 4 分钟,得到的还是"pom 写错了"这一个信息 ✗✗★ "失败即停"的逻辑:
后面的层依赖前面的层"至少能编译/能启动" ✓
前面失败了,后面必然失败 ——继续跑只是在浪费时间 ✓✓
★ 但有一个重要例外:
| 场景 | 该用哪种 |
|---|---|
| CI 的主流程(PR 门禁) | 失败即停 ✓ ——快速给结论 |
| 夜间全量跑 / 排查阶段 | 失败也继续(-NoFailFast)✓ ——一次收集全部问题 |
本项目的脚本两种都支持:
pwsh -File scripts\pipeline.ps1 # 失败即停(默认,模拟 CI 门禁)
pwsh -File scripts\pipeline.ps1 -NoFailFast # 跑完全部层,收集所有失败以 GitHub Actions 为例(其余 CI 大同小异):
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() 是最容易漏的一条:
流水线失败时,你想看报告;而报告步骤因为"失败即停"被跳过了 ✗✗ ——这是很讽刺的悖论 ✓
脚本:scripts/pipeline.ps1(对应 CI 配置,本地可复现)
# 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 函数会把内部命令的输出混进返回值
function Invoke-Stage {
& $Action
return $LASTEXITCODE # ← $code 会变成数组,`-ne 0` 恒为真
}后果:L1 明明通过,L2/L3 却被全部跳过 ✗ ——而日志显示"L1 通过" ✗✗(极具迷惑性)
正确写法:用 $script: 变量传出状态,不在函数里 return ✓(见脚本注释)
| # | 陷阱 | 现象 | 对策 |
|---|---|---|---|
| ① | 流水线时长失控 ✗✗ | 从 3 分钟涨到 40 分钟 → 大家不看结果了 | 设预算(如 10 分钟),超了就拆/并行/降频 ✓ |
| ② | flaky 用例 ✗✗ | 重跑一次就绿 → 大家学会"点重试" | 当缺陷处理(T2-011)✓ |
| ③ | 环境漂移 ✗ | 某天起全部失败 → 因为 CI 镜像变了 | 锁定版本(JDK/Maven/依赖)✓ |
| ④ | 报告没人看 ✗ | 失败了只看"红/绿",不看具体失败 | 归档报告 + 失败时贴关键行 ✓ |
★ 关于①,有个反直觉的规律:
流水线时长与"有人看结果"的比例成反比 ✓
3 分钟:大家会等结果再合并 ✓ 30 分钟:大家合并完就去干别的了,红绿灯形同虚设 ✗✗
这解释了为什么"分层 + 快速失败"如此重要:它保住了"人愿意等"这个前提 ✓✓
@echo off
cd /d "%~dp0..\labs"
call mvnw.cmd -B -ntp test || exit /b 1
echo 流水线通过见 5.2 的实测数据 ✓
全量跑(含 UI 与性能)→ 45 分钟 → 大家不看结果了 ✗脚本:scripts/pipeline.ps1(分层 + 失败即停 + 耗时统计)
运行(建议用 pwsh,见 .codebuddy/rules/build-and-verify.md 约束 4):
pwsh -File scripts\pipeline.ps1 -SkipBuild # 跳过站点构建,约 2.5 分钟
pwsh -File scripts\pipeline.ps1 # 含站点构建,约 5 分钟实测输出(真实运行结果):
===== L1 登记表与结构校验 =====
===== L2 快速单元测试(纯 Java 模块) =====
===== L3 慢测试(Spring 模块:切片 + 集成) =====
===== 流水线结果 =====
阶段 耗时(秒) 结果
L1 登记表与结构校验 3.4 通过
L2 快速单元测试(纯 Java 模块) 65.9 通过
L3 慢测试(Spring 模块:切片 + 集成) 80.6 通过
合计 149.9L2 与 L3 的模块构成:
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★ 注意"模块数"与"耗时"不成比例:
| 层 | 模块数 | 耗时 | 每模块平均 |
|---|---|---|---|
| L2 | 11 | 65.9 s | 6.0 s |
| L3 | 4 | 80.6 s | 20.2 s |
每模块成本差 3.4 倍 ✓ ——这才是"该分层"的真实依据 ✓✓
★ 一句可直接写进报告的结论:
"Spring 模块的单模块测试成本是纯 Java 模块的 3.4 倍(20.2s vs 6.0s), 因为它们要为每个测试类启动 Spring 容器。 所以新增测试时优先放在纯 Java 模块,除非确实需要容器。" ✓✓
-NoFailFast 重跑 → 对比"一次收集全部问题"与"快速失败"的时间差 ★★;-pl 参数:只构建指定模块,但 Maven 仍要解析全部模块的 pom ★(本轮踩过:一个模块的 pom 写错,导致所有 -pl 命令都失败)✓;| # | 错误 | 说明 |
|---|---|---|
| 1 | 流水线里没有静态检查 ✗ | 格式/引用问题要等到运行期才暴露 ✗ |
| 2 | 失败后继续跑完全部层 ✗✗ | 浪费时间(5.3)✗ |
| 3 | 报告不归档 ✗✗ | 失败时看不到细节,只能本地复现 ✗ |
| 4 | 忘了 if: always() ✗✗ | 失败时报告步骤被跳过(5.4)✗✗ |
| 5 | 不缓存依赖 ✗ | CI 大部分时间在下载 ✗ |
| 6 | 流水线时长无预算 ✗✗ | 涨到 40 分钟 → 没人看结果 ✗(5.6①) |
| 7 | 以为"分层 = 分快慢" ✗ | 真正的依据是"失败原因不同" ✓(5.1) |
目标:亲手搭一条分层流水线,并量出每层的成本。
步骤:
scripts/pipeline.ps1 跑一次,记录三层的耗时 ✓;if: always())✓。验收方式:能说出"你这三层的耗时各是多少、为什么这样分"。
扩展任务:给流水线加一层 L0(提交信息检查):
uses: actions/upload-artifact@v4(没有 if: always()),会有什么后果?pipeline.ps1 加"每层结束后输出该层的测试统计"(从 surefire 报告读 Tests run)。ui-testing 的 pom 写错而失败,L3 被跳过,省下 80.6 秒 ✓Invoke-Stage 之后读 labs/*/target/surefire-reports/*.txt,累加 Tests run 的数字 ✓if: always() 的 CI YAML ★;《CI 分层执行与报告归档》:把测试结果变成"可归档、可追溯"的证据。