质量门禁常被当成"正义"来讨论:卡得越严越好、覆盖率越高越好。但它本质是一笔投资——每道门禁都在消耗开发者等待时间,换回的是更晚发现的缺陷比例下降。投多了,团队开始绕过;投少了,故障成本落在用户侧。
下面按"钱花在哪、门禁卡什么、卡多严、怎么衡量"的顺序拆。
1. 质量成本的四类与放大机制
| 类别 | 内容 | 举例 | 可预测性 |
|---|---|---|---|
| 预防 | 评审、设计、类型系统、规范 | code review、契约先行 | 高,可预算 |
| 检测 | 测试、扫描、演练 | 单测、集成、压测 | 中,随覆盖面上升递减 |
| 内部失败 | 缺陷在交付前被发现 | 返工、阻塞合并 | 低,突发 |
| 外部失败 | 缺陷到了用户手里 | 故障、赔付、口碑 | 极低,最贵 |
# 缺陷成本模型:只用来做相对比较,参数按团队历史数据校准
def cost(defect, stage_found, stage_injected, base=1.0, growth=6.0):
return base * (growth ** (stage_found - stage_injected))
STAGES = ["需求", "设计", "编码", "测试", "灰度", "生产"]
# 同一缺陷:编码期发现 cost=6,生产发现 cost=1296(6^4,量级示意)
放大系数不是物理常数,但方向稳定:越往后,修复要动的面越大(数据修复、兼容、用户沟通)。这就是"左移"的经济学依据——不是为了好看,是因为右侧的单价高一个数量级。
2. 金字塔与冰淇淋筒
| 维度 | 测试金字塔 | 冰淇淋筒(反模式) |
|---|---|---|
| 主体 | 大量单测 + 少量集成 | 大量端到端/手工 |
| 反馈速度 | 秒级 | 分钟到小时 |
| 定位缺陷 | 精确到函数 | 只知道"这条路径坏了" |
| 维护成本 | 低,随重构同步 | 高,UI 一变全红 |
| 稳定性 | 高 | 高波动,随机失败 |
"分层比例"没有通用答案,按系统类型定:
| 系统类型 | 单测 | 集成/契约 | 端到端 | 说明 |
|---|---|---|---|---|
| 业务后端 | 多 | 中 | 少(关键链路) | 逻辑集中在服务层 |
| 基础库/SDK | 极多 | 少 | 无 | 调用方就是集成方 |
| 前端 | 中(组件) | 少 | 少(关键流程) | 视觉回归单独算 |
| 数据管道 | 中(转换函数) | 多(样例数据) | 少 | 断言落在数据契约 |
| 强监管系统 | 多 | 多 | 多(可追溯) | 证据链本身就是交付物 |
比例不是教条。判断标准是缺陷主要在哪一层产生:如果线上问题八成来自"两个服务对字段理解不一致",那契约测试的边际收益远高于再加一千条单测。
3. 门禁卡什么,每道花多久
| 门禁 | 耗时预算 | 失败处理 | 阻断合并 |
|---|---|---|---|
| 编译/类型检查 | 十秒到两分钟 | 立即失败 | 是 |
| 静态检查/lint | 一分钟内 | 只报新增问题 | 是(新增) |
| 单测 | 三到五分钟 | 失败即停 | 是 |
| 契约测试 | 两到五分钟 | 失败即停 | 是 |
| 集成测试(容器) | 五到十五分钟 | 重试一次后失败 | 是 |
| 性能基线 | 十分钟量级 | 超阈值告警 | 视波动定 |
| 安全扫描 | 五分钟到半小时 | 高危阻断 | 是(高危) |
# 分级超时:任何一道门禁超过预算,视为基础设施问题而非代码问题
stages:
- name: unit
timeout: 5m
retry: 0
- name: contract
timeout: 5m
needs: [unit]
- name: integration
timeout: 15m
retry: 1
耗时预算的意义在于总时长决定合并频率。PR 级门禁合起来超过十五分钟,开发者就会开始并行开多个 PR、批量合并,反馈回路被拉长,缺陷定位变难。想加快,先砍门禁数量,再谈并行化。
4. 门禁太严的后果与分级
| 症状 | 机制 | 对策 |
|---|---|---|
| 开发者本地跳过钩子 | 等待成本 > 违规成本 | 缩短 PR 级门禁 |
| 合并队列堵塞 | 串行执行、单点资源 | 分片并行、按影响面选跑 |
| "一键重跑"成为习惯 | 随机失败被当成噪声 | 隔离 flaky,设失败配额 |
| 例外审批泛滥 | 规则与真实风险不匹配 | 分级门禁,别一刀切 |
| 大 PR 越攒越大 | 门禁慢导致不愿小步提交 | 主干级门禁异步化 |
# 三级门禁:PR 快、主干全、发布严
pr: [compile, lint-new, unit-affected, contract]
main: [unit-all, integration, coverage-delta, security-low]
release: [perf-baseline, e2e-critical, security-high, migration-dry-run]
关键设计是按影响面选跑:改了 order 模块只跑该模块及其下游契约,不是全量。全量留给主干与发布级。这样 PR 级能在数分钟内完成,而风险并没有被让渡出去。
5. 发布节奏决定自动化下限
| 节奏 | 单测 | 集成 | 回滚 | 人工卡点 |
|---|---|---|---|---|
| 季度发布 | 可宽松 | 可手工 | 可有可无 | 多 |
| 周发布 | 必须稳定 | 必须自动化 | 必须有 | 少 |
| 日发布 | 必须快且稳 | 必须容器化 | 必须一键 | 极少 |
| 每日多次 | 必须可并行 | 必须可隔离 | 必须可自动判定 | 无 |
节奏越快,对"人"的依赖必须越少,因为人成了吞吐瓶颈。反过来,季度发布强行上全自动流水线,投入产出比通常不划算——发布次数太少,摊不平建设成本。这条常被忽略:自动化程度应该匹配发布频率,不是匹配"先进程度"。
6. 灰度与回滚:把风险从预防转到控制
| 手段 | 作用 | 成本 | 失效场景 |
|---|---|---|---|
| 灰度分批 | 缩小爆炸半径 | 需要流量路由 | 用户量大且同质,样本不具代表性 |
| 特性开关 | 不发版关功能 | 开关治理成本 | 开关堆积,组合爆炸 |
| 自动回滚 | 缩短恢复时间 | 需要可靠指标 | 指标滞后或本身被污染 |
| 蓝绿 | 秒级切换 | 双份资源 | 有状态服务、数据库变更 |
# 回滚判定:错误率与延迟同时越线才动,避免单指标抖动误触
if err_rate > 0.02 && p99 > 800ms; then rollback; fi
if err_rate > 0.05; then rollback; fi # 单指标硬阈值
灰度的经济学含义是:用一小部分用户的风险,换取对全量用户的确定性。它不能替代测试,但能替代一部分"上线前必须百分百确定"的焦虑——而后者往往要靠无限期延期来满足。
7. 指标怎么选
| 指标 | 定义 | 用途 | 被玩坏的方式 |
|---|---|---|---|
| 逃逸缺陷率 | 上线后发现的缺陷 / 总缺陷 | 衡量检测有效性 | 少报、改分类 |
| 变更失败率 | 导致回滚或热修的发布占比 | 衡量发布质量 | 只统计"严重"故障 |
| 平均恢复时间 | 从告警到恢复 | 衡量控制能力 | 提前关闭告警 |
| 门禁拦截率 | 被门禁挡下的变更占比 | 衡量门禁有效性 | 追求高拦截率而加严 |
覆盖率当 KPI 的失效很典型:它度量"被执行的行",不度量"被验证的行为"。团队会写不含断言的测试、给 getter 补调用、把难测逻辑挪进 if __name__ 之外的脚本,覆盖率涨了,缺陷率不变。
正确的用法是把覆盖率当诊断指标而非目标:某模块覆盖率骤降 → 问为什么;而不是"本月必须到 80%"。
8. 测试债:删什么、补什么
| 类型 | 判断 | 处理 |
|---|---|---|
| 永不变红的断言型测试 | 是否还在保护行为 | 保留;它便宜 |
| 频繁 flaky | 失败是否可复现 | 隔离 → 修 → 修不好就删 |
| 覆盖已删代码 | 目标是否还存在 | 删 |
| 巨型端到端 | 能否拆成契约 + 单测 | 拆,保留关键路径 |
| 无断言测试 | 只跑不断言 | 删或补断言 |
# 找出最慢与最常跳过的用例:测试债的两个主要来源
pytest --durations=20 -q
grep -rn "skip\|xfail" tests/ | wc -l
测试债要量化才有讨论基础:CI 总时长、flaky 率、被跳过用例数、单条用例平均维护次数。没有数字,"测试太多/太少"只是立场。
9. 组织:谁负责门禁,例外怎么留痕
| 角色 | 职责 | 不该做 |
|---|---|---|
| 平台/基建 | 维护流水线、耗时、稳定性 | 替团队决定测试策略 |
| 模块 owner | 定本模块门禁内容 | 私自关掉全局门禁 |
| 发布经理 | 判定放行、组织回滚 | 长期豁免 |
| 质量负责人 | 指标口径、复盘 | 用指标排名 |
# 例外必须有时限与责任人,否则等于永久关闭
exception:
gate: perf-baseline
reason: 依赖上游接口改造,基线暂不可比
owner: team-order
expires: 2026-12-01
ticket: OPS-1841
compensating: 灰度 5% + 人工观察 24h
例外审批的价值不在于"批",而在于留下补偿措施。没有补偿措施的豁免,就是把风险静默地转移到用户侧。
10. 排错
| 现象 | 原因 | 处理 |
|---|---|---|
| 门禁时快时慢 | 共享 runner 争抢、缓存未命中 | 隔离资源、固定依赖锁 |
| 本地过、CI 不过 | 环境差异、时区/随机种子 | 容器化、固定种子与时钟 |
| 每次都全量跑 | 未做影响面分析 | 按模块与依赖图选跑 |
| 红灯长期挂着 | 无 owner、无人清理 | 设失败配额,超限冻结合并 |
| 覆盖率涨、缺陷不降 | 断言缺失 | 改看变异测试或逃逸缺陷率 |
11. 边界
| 场景 | 更合适 | 理由 |
|---|---|---|
| 研究型/探索型项目 | 少门禁 + 频繁实验 | 需求本身在变,测试目标不稳定 |
| 一次性脚本 | 无门禁 | 生命周期短于建设成本 |
| 强监管行业 | 门禁 + 证据留档 | 合规证据是交付物 |
| 单人项目 | 单测 + 提交前钩子 | 流程成本占比过高 |
| 遗留系统改造 | 先补特征测试 | 没有安全网不敢重构 |
质量门禁的本质是用确定的时间成本,换不确定的故障成本。当故障成本足够低(内部工具、一次性脚本),或者不确定到无法评估(早期探索),把时间花在门禁上就是负收益。
12. 自测
- 四类质量成本里,哪一类最难预算?为什么?
- "左移"的经济学依据是什么?放大系数为什么不能当常数用?
- 测试分层比例应该由什么决定,而不是由什么决定?
- PR 级门禁总时长超过十五分钟,会引发哪些连锁反应?
- 为什么覆盖率适合当诊断指标而不适合当 KPI?
- 例外审批里最不能省的一项是什么?