幕 V · 落地CH 22进度 22/24

CI 里的测试:什么时候跑什么

一个测试写得再好,如果只在你想起来的时候手动跑一下,它的价值就打了骨折。测试真正的力量,来自三个词:自动、每次、挡得住合并。而实现这三个词的地方,是持续集成(CI)。CI 把测试从"一个偶尔跑跑的脚本",变成一道"红了就进不来"的闸门 —— 这才是测试能保护一个团队的方式。这一章讲怎么在 CI 里编排测试:哪些秒级跑、哪些分钟级跑、哪些留到夜里;怎么用并行把 20 分钟压到 3 分钟;以及那个所有人都会遇到的难题 —— flaky 测试卡在门禁上,该拿它怎么办。

自动化即闸门分层流水线分片并行必需检查flaky 隔离区

CI 是测试的执行引擎

先说清楚 CI 给测试带来的三个质变,这是它值得单独一章的原因:

◆ 判词 · 自动 + 每次 + 门禁 = 测试真正生效
  • 自动:每次 push、每个 PR,测试自动跑。不靠任何人"记得跑" —— 人是会忘的,机器不会。
  • 每次:每一处改动都被验证。一个 bug 被引入的那一刻,而不是三周后,就被抓住。
  • 门禁:测试红,代码就合不进主干。这是从"测试是建议"到"测试是规则"的关键一跃。

没有 CI,你写的测试只是一堆建议;有了 CI 的门禁,它们才成了不可绕过的验证。第 1 章说测试是"外部真相来源",而 CI 是这个真相被强制执行的地方。

分层流水线:快的在前,慢的在后

第 8 章的"测试选择台"Demo,画的就是这件事。核心原则是快速失败:把又快又能抓大错的测试放最前面,让明显的问题尽早、尽廉价地暴露。

关卡跑什么时长目标
本地保存时相关的单元测试秒级不打断心流,即时反馈
提交前 pre-commit冒烟 + 快速单元 + lint十几秒挡住蠢错误再上传
PR 检查冒烟 + 全部单元 + 集成几分钟合并前的主闸门
合并到主干加上端到端十几分钟全量,允许慢
每夜构建加上变异测试、慢/隔离的怪测试很久深度审计,没人等
◆ 判词 · 越靠前越快越频繁,越靠后越全越慢

这个结构的智慧,是让每一层都只承担它性价比最高的活:本地秒级反馈靠单元、PR 门禁靠单元+集成、昂贵的端到端和变异测试留到"没人等着它"的合并后/夜间。冒烟(第 8 章)在每一关最前面,先花几秒确认"地基没塌"。这样,90% 的错误在几秒内就被最便宜的关卡挡住,只有需要深度检查的才走到后面。

分片并行:把 20 分钟压成 3 分钟

当测试多到一台机器跑要 20 分钟,答案不是"忍",而是分片(sharding):把测试拆成 N 份,丢到 N 台机器上同时跑。测试天生适合并行 —— 只要它们真的独立(FIRST 的 I,第 5 章)。

分片:8 份并行20min → ~3min
串行 1 台 20 min
并行 8 台 ~3 min
⚠ 陷阱 · 分片会揪出你藏着的"相互依赖"测试

分片有个副作用,其实是好事:它会暴露那些偷偷依赖执行顺序/共享状态的测试(第 5、17 章)。因为分到不同机器后,测试 B 再也拿不到测试 A 留下的状态了 —— 它当场红给你看。如果你一开分片就一堆莫名其妙的红,恭喜,你发现了一批一直潜伏的 flaky 隐患。

必需检查:红了就是进不来

门禁要真的是门禁,得配上仓库的分支保护(branch protection):把关键的 CI 检查设为必需(required)。这样,只要这些检查是红的,PR 的合并按钮就是灰的 —— 没有人能绕过,包括你自己在赶时间的时候。

这一步很关键,因为它把"要不要理会红灯"这个需要意志力的决定,变成了一个系统强制的规则。意志力会在 deadline 前失守,规则不会。

flaky 隔离区:别让一个抖动的测试卡死所有人

把第 17 章的 flaky 治理,落到 CI 的具体机制上:

◆ 判词 · 隔离,而不是重试;修复,而不是放任
  • 探测:CI 记录每个测试的历史通过率。自动标出那些"代码没动却时红时绿"的测试。
  • 隔离:把确认 flaky 的测试移出必需检查,放进一个单独的、不阻塞合并的"隔离区"任务里继续跑。这一步保住了主门禁的可信度 —— 主流水线里,红就是真的红,大家才会认真对待。
  • 限期修复:隔离区不是垃圾桶。给每个进去的测试定个修复期限,消除它的非确定性根因(而不是加重试,第 17 章)。修不好的,删掉或重写。

反面教材是"全局失败重试":它掩盖 flaky、也掩盖真 bug、还拖慢一切(第 17 章)。隔离区的精髓是把 flaky 挪出门禁,但不放过它

进阶:只跑改动相关的测试

大项目里,还有一招能进一步提速:测试影响分析(test impact analysis) —— 根据这次改了哪些文件,只跑可能受影响的那部分测试。改了一个孤立的工具函数,没必要跑整个 App 的端到端。这需要构建系统支持依赖分析(Gradle、Bazel 都有相关能力),属于规模上来后的优化,但方向和整章一致:用最小的代价,拿到足够的信心。

AI 时代:CI 是 AI 代码的最后一道闸

▲ AI 产出越快,CI 这道闸越重要

回到主线。AI 让代码以前所未有的速度涌入你的仓库 —— 而它产出的每一行,都带着第 1 章说的那种"不携带正确性信息的自信"。CI 正是那个不由自信、只由事实说话的关卡:AI 的代码再漂亮、它的解释再流畅,过不了这道自动化的验证,就进不了主干。

这也让一件事变得更重要:CI 里跑的那套测试,本身必须是可信的(有牙齿、不 flaky、不是 AI 自己写来给自己放水的,第 19 章)。否则闸门形同虚设 —— 一道由"假装在验证"的测试把守的门,拦不住任何东西。CI 是"外部真相来源"真正上岗执勤的地方,而它执勤的质量,取决于你前二十一章打下的所有底子。

一份真实的分层流水线

把这一章的原则落成一份能抄的 GitHub Actions 配置。注意三个要点:冒烟前置、分片矩阵、以及分片结果如何汇总:

jobs:
  smoke:                              # 关卡一:最前、最快
    runs-on: ubuntu-latest
    steps: [ { run: ./gradlew smokeTest } ]

  unit:                               # 关卡二:分片并行
    needs: smoke                      # 冒烟绿了才跑
    strategy:
      matrix: { shard: [1, 2, 3, 4] } # ← 4 台机器同时跑
    runs-on: ubuntu-latest
    steps:
      - run: ./gradlew test -Pshard=${{ matrix.shard }} -PtotalShards=4

  integration:
    needs: unit
    runs-on: ubuntu-latest
    steps: [ { run: ./gradlew integrationTest } ]

  gate:                               # 汇总:所有关卡都绿,才算这个 PR 通过
    needs: [smoke, unit, integration]
    runs-on: ubuntu-latest
    steps: [ { run: echo "all green" } ]

gate 这个「汇总」任务设为唯一的必需检查,是个实用技巧:分支保护只认 gate 一个,而 gate 依赖前面所有关卡。这样你增删测试任务时,不用每次去改分支保护规则,只要它们挂在 gateneeds 里就行。

合并队列:别让「各自绿」骗了你

分层流水线还挡不住一类阴险的问题:PR A 和 PR B 各自跑测试都绿,但它们合到一起就冲突(A 删了一个函数,B 刚好在别处调了它)。两个 PR 分别基于旧主干测的,谁也没测过「A+B 合并后」的状态。

◆ 判词 · 合并队列:按合并后的状态再测一遍

合并队列(merge queue)把待合并的 PR 排成一队,依次把「合并之后的结果」再跑一遍测试,绿了才真正落进主干。它挡住的正是「各自绿、合起来红」这类只有在集成那一刻才暴露的问题 —— 让主干始终是绿的,而不是"每个 PR 合的时候是绿的"。团队一大、并行 PR 一多,这几乎是必需品。

✚ 动手

1. 看你项目 CI 的分层:是所有测试一锅端,还是分了快慢?PR 门禁跑要多久?超过 10 分钟,考虑分片或下沉。

2. 确认你的关键检查是不是"必需"的:红了到底能不能强行合并?如果能,那门禁只是个摆设。

3. 你团队有没有 flaky 隔离机制?还是一律靠"重跑"?把最爱抖的那个测试,要么修好,要么隔离。

流水线搭好了,但它跑的测试会随时间腐烂 —— 越来越慢、越来越难懂、越来越难改。倒数第二章讲测试的可维护性:怎么让测试跑得快、活得久,而不是变成一堆没人敢动、又拖慢一切的负担。