这个风险该由哪一层测试发现
团队定了一个测试比例:单元测试 70%、集成测试 20%、端到端测试 10%。比例达标了,线上事故却大多出在「每一层都以为别人测过」的地方:单元测试用替身绕过了数据库约束,集成测试只测了单个接口,端到端测试只走了正常流程。
测试金字塔、测试奖杯这些形状,描述的是一种经验上的成本分布:越靠近底层的测试越快、越稳定、越便宜。但形状本身不回答最关键的问题:某个具体的风险,应该由哪一层的测试发现? 按比例分配测试,很容易让每一层都测「容易测的东西」,而真正的风险落在层与层之间的缝隙里。
本文给出一个从风险出发选择测试层次的方法:先列出风险,再问发现它需要哪些真实的组件,能用替身代替的部分有哪些,最后写下这一层没有覆盖的边界。这是一篇方法文,文中的实测引用自站内其他文章。
一、从风险出发,而不是从层次出发
对一个功能(例如「下单」),先列出可能出错的地方,每一条写成具体的失败情况:
| 风险 | 失败的样子 |
|---|---|
| 价格计算规则错了 | 满减叠加时多减了 10 元 |
| 同一个请求被处理两次 | 重试导致重复扣款 |
| 数据库约束没有生效 | 同一张优惠券被两个订单使用 |
| 下游超时 | 支付服务 3 秒不响应,下单接口挂起 30 秒 |
| 跨步骤的状态 | 下单后立刻取消,库存没有释放 |
| 外部接口格式变化 | 支付回调的字段改了名,状态解析失败 |
| 部署配置 | 生产环境的连接池大小沿用了默认值 |
这张表是选择测试的起点。每一行都要回答下面三个问题。
二、发现它需要哪些真实的组件
一个风险能被哪一层发现,取决于它涉及哪些组件的真实行为:
- 价格规则只涉及纯计算,单元测试就能发现,而且应该在这里发现:例子多、边界多,单元测试最便宜。
- 数据库约束涉及真实的数据库。用内存替身的单元测试发现不了,用测试事务的集成测试也可能发现不了,因为约束要到提交时才检查,见 Spring 测试事务让结果失真的五种方式。
- 重复处理涉及重试的发起方和幂等的接收方,要么在接收方用真实存储测试幂等键,要么在一个能注入重试的集成环境里测试。
- 外部接口格式涉及对方真实的行为,录制的响应只能证明过去的格式,见替身证明了什么。
- 部署配置只有在接近生产的环境里才能发现,或者用配置检查(启动时校验、配置的单元测试)提前拦截。
原则是:在包含所有必要真实组件的最低一层发现它。低于这一层,替身会掩盖风险;高于这一层,测试变得慢而脆弱,失败时也更难定位。
三、单个接口与跨步骤的状态
接口测试最常见的盲区是只测单个接口:每个接口的请求和响应都对,但几个接口组合起来的状态是错的。
- 单接口的边界:参数校验、错误码、权限、幂等。这些在一次请求内就能验证。
- 跨步骤的状态:下单—支付—取消—退款,每一步的前置条件依赖前面步骤留下的状态。「下单后立刻取消,库存是否释放」这类问题,只测单个接口永远发现不了。
跨步骤的测试要显式地设计状态的传递:前一步的输出(订单号、令牌)作为后一步的输入,而不是在每一步都准备一份独立的数据。它们数量不必多,但要覆盖状态机里有风险的转换路径。状态转换的系统化检查,见状态机与工作流。
四、替身代替了什么,就承担了什么
每一层测试都会用替身代替一部分组件。选择替身时,同时写下这个替身让哪些风险变得不可见:
| 测试 | 替身 | 因此看不到的风险 |
|---|---|---|
| 服务的单元测试 | 内存仓储 | 约束、排序、并发 |
| 带测试事务的集成测试 | 测试事务代替提交 | 约束时机、提交后回调、其他事务的可见性 |
| 接口的集成测试 | 支付服务的模拟服务器 | 对方真实的格式与超时行为 |
| 端到端测试 | 测试环境的数据与配置 | 生产规模的数据、生产配置 |
一个常见的推论是「上层测试失败了,下层一定有测试也失败」,所以下层测试足够多就可以少写上层测试。这个推论不成立:接线、配置、序列化、协议细节、事务时机这类缺陷,只在把真实组件连起来的那一层出现,下层每个组件单独看都是对的。拆分测试时覆盖责任去了哪里的实验里,故意让容器丢掉限定符,用桩的注入规则测试全部通过,只有真实容器的装配测试失败。
这张表的每一行都应该能在别处找到对应的测试:内存仓储看不到的约束,由使用真实数据库的契约测试覆盖;模拟服务器看不到的格式变化,由共享契约或定期的真实调用覆盖。找不到对应测试的,就是缝隙。
五、写下没有覆盖的边界
最后,承认有些风险不由任何自动化测试发现,并写下来:
- 生产规模的性能:由压测和上线后的监控发现,见固定 10 个并发压测,服务卡了 2 秒,报告里只有 10 个慢请求。
- 真实网络的分区与抖动:由故障演练发现。
- 第三方的行为变化:由监控和告警发现。
写下来的意义在于,这些风险有了明确的发现手段和负责人,而不是被假定为「测试覆盖了」。覆盖率数字也说明不了这一点,见覆盖率 100%,一个断言都没有。
六、关于比例
按上面的方法选完测试之后,再看各层的数量,通常会自然地呈现金字塔或奖杯的形状:规则密集的代码有大量单元测试,涉及存储和外部系统的地方有集成测试,少数关键流程有端到端测试。形状是结果,不是目标。把比例作为指标反过来要求团队,最省事的达标方式是给容易测的代码多写测试,而缝隙原样留着。
七、检查清单
- 先列风险,每条写成具体的失败情况。
- 每条风险在包含所有必要真实组件的最低一层发现。
- 接口测试覆盖单接口边界,也覆盖有风险的跨步骤状态。
- 每个替身都写明它让哪些风险不可见,并在别处找到对应的测试。
- 没有自动化测试覆盖的风险,写明发现手段和负责人。
- 测试的层次比例是结果,不作为指标。
参考资料
- Martin Fowler,The Practical Test Pyramid
- Kent C. Dodds,The Testing Trophy and Testing Classifications
- Google Testing Blog,Test Sizes