2839 字
约 9 分钟
19
S16-A06 · 测试策略:测试金字塔的真实成本
2024-12-31
2026-11-09

单元测试写了两千个,线上还是天天出事——这不是"测得不够",是测试的分布放错了位置。金字塔被引用得太多,被理解得太少:它描述的是反馈速度与单位缺陷成本的关系,不是"单元测试该占 70%"这种数量配比。把它当成配额表去凑数,得到的就是一个跑得快、拦不住的测试集。


1. 金字塔描述的是成本曲线,不是比例

类型 反馈速度 稳定性 能拦住的缺陷 单次维护成本
单元 毫秒级 高(无外部依赖) 分支逻辑、边界条件、算法错误 低,但与被测代码强耦合
集成 秒级到十秒级 中(依赖容器/网络/时钟) 契约错配、SQL 语义、序列化、事务边界 中,环境漂移会持续消耗
端到端 分钟级 低(UI/网络/时序) 装配错误、配置错误、跨服务链路断裂 高,定位成本远超修复成本

判断依据不是数量,是这条测试失败时,你能不能在一分钟内说出原因。说不出来的测试,无论在哪一层,都是负债。

2. 单元测试该覆盖分支与边界,不是每个方法

"每个方法一个测试"产出的是覆盖率幻觉:getter 有测试,if (amount > limit) 的两个方向却只有一个被测。真正的分母是分支与边界组合,不是方法数。

@ParameterizedTest
@CsvSource({ "0,false", "1,true", "9999,true", "10000,true", "10001,false" })
void shouldRejectOverLimit(long amount, boolean expected) {
    assertEquals(expected, policy.check(amount)); // 边界值各测一侧
}

参数化测试的价值在于让边界显式成表:上面这张表本身就是需求文档。缺陷高发区通常是四类——空值、临界值(0、max、max+1)、状态迁移(未支付→已支付)、并发交错。这四类不覆盖,测一百个方法也没用。

反过来,纯委托方法(return repo.find(id))的测试价值接近零:它只能拦住"有人改了调用目标",而这种改动通常伴随编译错误。

3. 集成测试的收益峰值在跨边界契约

边界 值得写集成测试的理由 不值得的理由
数据库访问 SQL 语义、索引、事务传播、类型映射,mock 全部测不到 单表 CRUD 且已有框架保证
外部 HTTP API 请求体形状、鉴权头、错误码分支 供应商稳定且契约测试已覆盖
序列化/反序列化 字段名、时区、精度、可选字段缺失 只在进程内传值
消息队列 投递语义、重试、死信、顺序 仅用于解耦且语义已由框架保证

关键判据:mock 掉的部分是否可能出错,且出错后单元测试仍然全绿。如果是,那这块必须有一条真集成测试。数据库时区、decimal 精度、外键级联这类问题,全部属于"mock 永远正确,真实环境永远不对"。

4. 端到端为什么贵

成本项 表现 后果
脆弱 UI 选择器改动、异步渲染时序、测试数据被别的用例污染 一次重构要改几十条测试
慢 每条分钟级,串行跑一小时 只能在夜间跑,反馈延迟到次日
定位难 失败只告诉你"首页打不开" 排查时间常常超过修复时间
环境耦合 依赖共享测试环境、第三方沙箱 别人部署一次你就红一片

端到端该留的是关键路径,而且是"钱和登录"那类路径:

1. 登录 → 下单 → 支付回调 → 订单状态可见
2. 注册 → 邮件激活 → 首次登录
3. 退款 → 余额变动 → 账单可查

三条够了。第四条开始,边际收益迅速下降,维护成本线性上升。端到端的价值不是"覆盖广",是"证明装配没坏"——它测的是接线,不是逻辑。

5. 断言行为,不要断言实现

// 坏:断言实现细节,重构即红
verify(repo, times(2)).save(any());
assertEquals("{\"id\":1,\"name\":\"a\"}", json);

// 好:断言可观察行为
assertEquals(OrderStatus.PAID, order.getStatus());
assertThat(parsed.name()).isEqualTo("a"); // 解析成对象再比字段

两种断言的区别在于重构自由度:断言实现细节的测试会在你改内部结构时集体爆红,于是团队学会"改测试比改代码快"——测试从此失去警戒作用,只剩仪式感。

测试替身同理。为一个类注入五个 mock,说明这个类的依赖边界有问题;测一个流程要摆十个 mock,说明该测的是契约而不是协作顺序。mock 断言调用次数是最典型的实现耦合,除非次数本身就是业务约束(比如"只扣一次款")。

6. flaky 的处置纪律:隔离、修复或删除

处置 适用 代价
立即隔离(独立流水线/标签) 影响主干、原因未明 覆盖真空,必须设复查期限
修复(定位根因) 时钟、异步、共享状态、端口冲突 通常要改生产代码的可测性
删除 价值低、成本高于收益 需要记录决策,避免反复重建
重试掩盖 任何情况 把偶发缺陷变成统计噪声,最坏的选择

重试的唯一"合理"场景是外部不可控因素(第三方沙箱抖动),且必须记录重试次数并单独告警——重试率上升本身就是信号。

flaky 的根因清单很短:没固定时钟、没隔离数据、没等待条件(用 sleep 代替条件等待)、共享端口、测试间有顺序依赖,这五项占了绝大多数。隔离要落在构建配置里,而不是靠口头约定:

<plugin>
  <artifactId>maven-surefire-plugin</artifactId>
  <configuration>
    <excludedGroups>flaky</excludedGroups>   <!-- 主干流水线先排除 -->
  </configuration>
</plugin>

被排除的用例必须进一张有复查期限的清单,否则"隔离"就是删除的委婉说法。

7. 覆盖率作为指标的失效方式

失效模式 表现 为什么指标看不出来
断言空洞 调用了但没断言,或只断言 assertNotNull 行覆盖照样 100%
追行数 为覆盖 catch 块写空测试 覆盖率涨,缺陷率不动
排除配置 把难测模块加进排除列表 报表变好看,风险留在原地
单点指标 只看总覆盖率,不看增量 老代码拉高均值,新代码裸奔

覆盖率当下限守门是合理的(比如新增代码不低于某阈值),当目标就会立刻被优化。更可靠的替代指标:逃逸缺陷数(上线后发现的、本该被测试拦住的缺陷)、测试失败的平均定位时长、flaky 率。

8. 执行位置决定反馈延迟

位置 延迟 适合放什么 不适合放什么
本地提交前 秒级 单元测试、静态检查、格式化 集成与 E2E
CI 每次推送 分钟级 单元 + 集成 + 契约测试 全量 E2E
合并前门禁 十分钟级 关键路径 E2E、迁移校验 性能测试
预发/夜间 小时级 全量 E2E、压测、混沌实验 需要快速反馈的任何东西

延迟超过十分钟的检查,人就会绕过去。这不是纪律问题,是排队论:反馈越晚,上下文重建成本越高,绕过它的动机越强。

9. 遗留系统补测试的入口

不要从"补单元测试"开始,从**特征测试(characterization test)**开始:先把当前行为(包括你认为错误的行为)钉住,再重构。

@Test
void currentBehaviorIsPinned() {
    // 不判断对错,只记录现状,重构后必须仍一致
    String out = legacy.calculate(input); // 输入取自生产样本
    assertEquals(GOLDEN_OUTPUT, out);
}

顺序:挑一条最核心的用例 → 用真实数据构造输入 → 记录输出为黄金值 → 加断言 → 再动代码。每补一条特征测试,就获得一小块可以安全重构的区域。反过来先重构再补测试,等于在没有护栏的路上换轮胎。

10. 排错:测试集本身的六种病

现象 根因 处理
改一行代码红一片 断言实现细节、mock 过度 改成断言行为,减少协作断言
CI 偶发失败、本地不复现 时钟、数据、端口、顺序依赖 固定时钟、每用例独立数据、随机端口
测试跑得比构建还慢 E2E 过多、容器重复启动 复用容器、分层执行、只留关键路径
覆盖率 90% 但线上照崩 断言空洞、只测 happy path 补分支与边界,加逃逸缺陷复盘
测试没人看失败 长期红灯、重试掩盖 清理并设门禁,红灯即阻断合并
新人不敢改测试 测试是唯一的需求文档且已腐坏 测试命名写行为,与需求编号关联

11. 边界

场景 判断
一次性脚本、数据迁移工具 不铺测试,跑一次即弃,用 dry-run 校验
纯展示页面、静态文案 断言快照即可,不值得写交互测试
探索期原型 先验证可行性,测试在需求稳定后再补
强监管/资金路径 测试密度应高于常规,尤其补偿与对账
团队只有两人 关键路径 E2E + 核心分支单元,别搭全套流水线

测试策略的失败很少是"测得少",多半是"测在了不产生信息的地方"。每加一条测试都该能回答:它失败时,我会因此改变什么?

12. 自测

  1. 金字塔的纵轴到底表示什么?为什么不是数量比例?
  2. 一个方法有五个分支,只写一个测试,覆盖率缺口在哪?
  3. mock 掉的部分满足什么条件时,必须补一条真集成测试?
  4. 端到端测试的价值是"覆盖广"还是别的什么?
  5. 为什么"断言 mock 调用次数"会在重构时制造阻力?
  6. 覆盖率作为下限和作为目标,行为差异体现在哪里?
S16-A06 · 测试策略:测试金字塔的真实成本
https://www.clxhxhhr.top/posts/6157/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。