自动化测试的 CI 流水线:从"手动跑"到"每次提交必过"

“这个改动会不会弄坏别的”——这句话在团队里每说一次,就是一次隐性成本。CI 流水线的存在就是让这句话不再需要说:每次提交自动验证,红了就不许合。这篇讲一条自动化测试 CI 流水线的完整设计,从触发到报告到门禁。

一、流水线结构:三层闸门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
提交 (git push / MR)


┌─────────────┐
│ L1 快速门禁 │ 编译 + 单测 + lint 目标:5 分钟内
│ (每次提交) │ 挂了直接红,不浪费后续资源
└──────┬──────┘
▼ 通过
┌─────────────┐
│ L2 集成验证 │ 接口自动化 + Mock 环境 目标:30 分钟内
│ (每次提交) │ 依赖起容器/服务,真调用
└──────┬──────┘
▼ 通过
┌─────────────┐
│ L3 回归+发布 │ UI 回归(设备池)+ 长稳 目标:发版前/每日
│ (每日/发版) │ 报告 → 发版门禁
└─────────────┘

分层的原则:越便宜越快越靠前。L1 挂了还跑 L3 是纯浪费——编译都过不了,UI 跑出来也是噪音。L1+L2 合并成"提交必过",L3 是"发版必过"。

二、Jenkins Pipeline 骨架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
pipeline {
agent { label 'test-linux' }

parameters {
choice(name: 'PROFILE', choices: ['smoke', 'api', 'regression'],
description: '测试档位(MR 默认 smoke,定时任务跑 regression)')
}

stages {
stage('Checkout') {
steps {
checkout scm
sh 'git rev-parse HEAD > build.env' // 记录版本指纹
}
}

stage('L1 快速门禁') {
parallel {
stage('编译') { steps { sh 'make build' } }
stage('单测') { steps { sh 'pytest -m "unit" -q --junitxml=report/unit.xml' } }
stage('Lint') { steps { sh 'ruff check . && black --check .' } }
}
}

stage('L2 集成验证') {
when { expression { params.PROFILE != 'smoke' || currentBuild.getBuildCauses().toString().contains('TimerTrigger') } }
environment {
BASE_URL: 'https://staging.api.internal' // 集成环境
ENV_FINGERPRINT: sh(script: 'python tools/env_fingerprint.py', returnStdout: true)
}
steps {
sh '''
pytest -m "${PROFILE}" -q \
--junitxml=report/integration.xml \
--html=report/integration.html --self-contained-html
'''
}
post {
always {
archiveArtifacts 'report/**' // 报告+截图归档,带 run_id
}
failure {
// 失败归因提示:环境指纹随消息发出,排查先 diff 指纹
dingtalkRobot(text: "集成失败 @${gitBranch()} ${ENV_FINGERPRINT}", at: committer)
}
}
}

stage('L3 UI 回归') {
when { expression { params.PROFILE == 'regression' } }
steps {
// 设备池调度:按平台分片并行(见设备农场那篇)
sh 'python tools/dispatch_ui_regression.py --profile regression'
}
}
}

post {
always {
// 结果汇总:JUnit XML → 趋势图 + 失败列表 → 推送
sh 'python tools/report_agg.py --build-id ${BUILD_NUMBER}'
}
}
}

三、四个决定流水线"可信度"的细节

1. 环境指纹随报告走

每次 run 的 ENV_FINGERPRINT(部署版本 + 配置 hash + 数据版本)写进报告头和失败消息。"环境是不是被人动过"从猜变成 diff——这是流水线跑红后最省时间的排查动作。

2. 失败必带现场,且可追溯

1
2
3
4
5
6
# 失败消息模板(钉钉/飞书)
[api] test_order_pay 失败(3/3 重试均失败)
断言: status == "PAID", got "TIMEOUT"
现场: <报告链接#用例锚点> # 截图 + 请求响应 + 相关日志
环境: staging @ a3f9c21 (seed-0910)
历史: 该用例近 7 2 5 绿(疑似 flaky,见清单)

"重试均失败"和"近 7 天红绿记录"是区分产品 bug 和 flaky 的第一手信息,没有历史数据的失败消息只是噪音。

3. flaky 隔离区:红了先隔离,不许带病放行

flaky 用例(间歇失败)是流水线公信力的头号杀手——团队一旦觉得"红了也正常",流水线就形同虚设。机制:

1
2
3
4
用例连续 2 次"重跑后通过" → 自动进隔离区(quarantine)
隔离区用例:不阻塞合并,但每 build 仍跑,结果单独统计
每周例会过隔离区:修稳定(出清单)或永久移除
隔离区 > 5% 用例总数 → 告警(流水线健康度指标)

关键:隔离不是"假装没发生",是"显式承认不稳定 + 限期治理"。

4. 门禁要有例外通道,但留痕

“紧急 hotfix 等不了 30 分钟集成"是真实需求。解法不是"绕过”,是显式降级 + 补跑

  • MR 打上 skip-ci 标签 → L2/L3 不阻塞合并,但合并后自动补跑,补跑红 = 回滚流程启动;
  • 所有跳过记录进周报(谁跳的、什么理由、补跑结果),例外透明化,否则门禁会被慢慢钻空。

四、流水线自身的度量

流水线也是产品,要自己看板:

指标 健康线 异常含义
L1 平均时长 <5min 超了 → 开发者会开始跳过
L2 平均时长 <30min 超了 → 拆并行/缩数据
flaky 率 <2% 超了 → 隔离区积压,公信力下降
失败→修复 MTTR <1day 超了 → 失败现场不够用
误报率(环境原因失败占比) <10% 超了 → 环境不稳定,先修环境

五、小结

CI 流水线的四个支柱:分层闸门(便宜的先跑)、环境指纹(排查先 diff)、flaky 隔离区(保住公信力)、透明例外(门禁能活下来)。最终衡量标准只有一个:开发者愿不愿意等它——等得值,门禁才有意义;等不起,再严格的门禁也形同虚设。