Skip to content

这个风险该由哪一层测试发现 ​

团队定了一个测试比例:单元测试 70%、集成测试 20%、端到端测试 10%。比例达标了,线上事故却大多出在「每一层都以为别人测过」的地方:单元测试用替身绕过了数据库约束,集成测试只测了单个接口,端到端测试只走了正常流程。

测试金字塔、测试奖杯这些形状,描述的是一种经验上的成本分布:越靠近底层的测试越快、越稳定、越便宜。但形状本身不回答最关键的问题:某个具体的风险,应该由哪一层的测试发现? 按比例分配测试,很容易让每一层都测「容易测的东西」,而真正的风险落在层与层之间的缝隙里。

本文给出一个从风险出发选择测试层次的方法:先列出风险,再问发现它需要哪些真实的组件,能用替身代替的部分有哪些,最后写下这一层没有覆盖的边界。这是一篇方法文,文中的实测引用自站内其他文章。

风险需要的真实组件最低的层次满减叠加多减了 10 元只有计算单元测试同一张优惠券被两单使用真实数据库 + 真实提交集成测试(不用测试事务)重试导致重复扣款接收方的幂等存储集成测试下单后取消,库存没释放多个接口 + 共享状态跨步骤的接口测试支付回调字段改了名对方真实的行为契约测试 / 定期真实调用生产连接池用了默认值生产配置启动校验 / 预发环境大促时 P99 超标生产规模与流量压测 + 监控每个替身都写下它让哪些风险不可见,并在别处找到覆盖它的测试;找不到的就是缝隙
图 1 · 示意:先列出具体的风险,再问发现它需要哪些真实的组件,在包含这些组件的最低一层测试它。低于这一层,替身会掩盖风险;高于这一层,测试慢而难以定位。没有任何自动化测试能覆盖的风险,写明由压测、演练或监控发现。

一、从风险出发,而不是从层次出发 ​

对一个功能(例如「下单」),先列出可能出错的地方,每一条写成具体的失败情况:

风险失败的样子
价格计算规则错了满减叠加时多减了 10 元
同一个请求被处理两次重试导致重复扣款
数据库约束没有生效同一张优惠券被两个订单使用
下游超时支付服务 3 秒不响应,下单接口挂起 30 秒
跨步骤的状态下单后立刻取消,库存没有释放
外部接口格式变化支付回调的字段改了名,状态解析失败
部署配置生产环境的连接池大小沿用了默认值

这张表是选择测试的起点。每一行都要回答下面三个问题。

二、发现它需要哪些真实的组件 ​

一个风险能被哪一层发现,取决于它涉及哪些组件的真实行为:

  • 价格规则只涉及纯计算,单元测试就能发现,而且应该在这里发现:例子多、边界多,单元测试最便宜。
  • 数据库约束涉及真实的数据库。用内存替身的单元测试发现不了,用测试事务的集成测试也可能发现不了,因为约束要到提交时才检查,见 Spring 测试事务让结果失真的五种方式。
  • 重复处理涉及重试的发起方和幂等的接收方,要么在接收方用真实存储测试幂等键,要么在一个能注入重试的集成环境里测试。
  • 外部接口格式涉及对方真实的行为,录制的响应只能证明过去的格式,见替身证明了什么。
  • 部署配置只有在接近生产的环境里才能发现,或者用配置检查(启动时校验、配置的单元测试)提前拦截。

原则是:在包含所有必要真实组件的最低一层发现它。低于这一层,替身会掩盖风险;高于这一层,测试变得慢而脆弱,失败时也更难定位。

三、单个接口与跨步骤的状态 ​

接口测试最常见的盲区是只测单个接口:每个接口的请求和响应都对,但几个接口组合起来的状态是错的。

  • 单接口的边界:参数校验、错误码、权限、幂等。这些在一次请求内就能验证。
  • 跨步骤的状态:下单—支付—取消—退款,每一步的前置条件依赖前面步骤留下的状态。「下单后立刻取消,库存是否释放」这类问题,只测单个接口永远发现不了。

跨步骤的测试要显式地设计状态的传递:前一步的输出(订单号、令牌)作为后一步的输入,而不是在每一步都准备一份独立的数据。它们数量不必多,但要覆盖状态机里有风险的转换路径。状态转换的系统化检查,见状态机与工作流。

四、替身代替了什么,就承担了什么 ​

每一层测试都会用替身代替一部分组件。选择替身时,同时写下这个替身让哪些风险变得不可见:

测试替身因此看不到的风险
服务的单元测试内存仓储约束、排序、并发
带测试事务的集成测试测试事务代替提交约束时机、提交后回调、其他事务的可见性
接口的集成测试支付服务的模拟服务器对方真实的格式与超时行为
端到端测试测试环境的数据与配置生产规模的数据、生产配置

一个常见的推论是「上层测试失败了,下层一定有测试也失败」,所以下层测试足够多就可以少写上层测试。这个推论不成立:接线、配置、序列化、协议细节、事务时机这类缺陷,只在把真实组件连起来的那一层出现,下层每个组件单独看都是对的。拆分测试时覆盖责任去了哪里的实验里,故意让容器丢掉限定符,用桩的注入规则测试全部通过,只有真实容器的装配测试失败。

这张表的每一行都应该能在别处找到对应的测试:内存仓储看不到的约束,由使用真实数据库的契约测试覆盖;模拟服务器看不到的格式变化,由共享契约或定期的真实调用覆盖。找不到对应测试的,就是缝隙。

五、写下没有覆盖的边界 ​

最后,承认有些风险不由任何自动化测试发现,并写下来:

写下来的意义在于,这些风险有了明确的发现手段和负责人,而不是被假定为「测试覆盖了」。覆盖率数字也说明不了这一点,见覆盖率 100%,一个断言都没有。

六、关于比例 ​

按上面的方法选完测试之后,再看各层的数量,通常会自然地呈现金字塔或奖杯的形状:规则密集的代码有大量单元测试,涉及存储和外部系统的地方有集成测试,少数关键流程有端到端测试。形状是结果,不是目标。把比例作为指标反过来要求团队,最省事的达标方式是给容易测的代码多写测试,而缝隙原样留着。

七、检查清单 ​

  • 先列风险,每条写成具体的失败情况。
  • 每条风险在包含所有必要真实组件的最低一层发现。
  • 接口测试覆盖单接口边界,也覆盖有风险的跨步骤状态。
  • 每个替身都写明它让哪些风险不可见,并在别处找到对应的测试。
  • 没有自动化测试覆盖的风险,写明发现手段和负责人。
  • 测试的层次比例是结果,不作为指标。

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权