跳到主要内容
约 10 分钟4 张卡

测试金字塔

测试金字塔按验证范围组织测试,让不同层级分别发现局部规则和组件协作中的错误。本文先介绍各层测试的范围与成本,再说明真实依赖和契约怎样验证。最后讨论回归测试、覆盖率和不稳定测试,帮助你按业务风险安排测试。

1. 面试核心​

先记住这几条,再读正文
  • 局部与集成验证。 单元测试验证局部规则,集成测试验证真实组件协作。卡 · 测试范围
  • 契约与端到端。 契约测试验证提供方实现是否满足消费方的接口约定,端到端测试验证完整业务流程。卡 · 消费方契约与服务端验证
  • 代码覆盖率。 JaCoCo 用行覆盖率记录哪些代码行已执行,用分支覆盖率记录哪些 if、switch 分支已执行。卡 · 回归测试
  • 缺陷回归。 回归测试保留缺陷触发条件,并验证修复后的可观察行为。
  • 断言有效性。 变异测试修改条件或返回值,检测断言能否识别错误。卡 · 变异测试结果

2. 测试范围​

选择测试类型时,先确定要验证的行为涉及哪些真实组件。下表列出了各层能提供的证据,以及需要其他层补充的部分。

类型主要证据常见盲区
单元测试一个行为单元的规则与边界框架配置、真实协议、数据库
组件或切片测试Web、数据访问等局部集成完整部署与跨服务链路
集成测试真实数据库、消息或客户端协作全系统用户流程
契约测试提供方与消费方接口兼容服务内部正确性、真实容量
端到端测试部署环境中的关键业务路径细粒度失败定位、所有组合

同一条支付流程需要验证四类行为,它们适合放在不同层级:

  • 金额舍入规则用单元测试,直接检查输入与计算结果。
  • 数据库列精度用集成测试,确认写入后是否发生截断或舍入。
  • 支付回调字段用契约测试,检查消费方与提供方是否兼容。
  • 真实下单主路径用少量端到端测试,确认部署后的组件能一起完成业务。

3. 底层测试的数量​

单元测试运行快、失败定位清楚,适合覆盖大量分支和边界。端到端测试还要协调进程、网络、数据和异步任务。因此,扩大测试范围会增加执行与定位成本:

  • 测试需要启动和连接更多组件,环境更难建立。
  • 一次用例经过的环节更多,运行时间更长。
  • 网络、进程和业务都能导致失败,因此定位原因更难。
  • 多个用例共用完整环境时,测试数据更易冲突。
  • 重试后通过会掩盖偶发失败,使不稳定因素继续存在。

这些成本使测试组合呈现底层多、上层少的金字塔形状。具体分布仍由业务风险决定:

  • 纯规则多的系统适合大量单元测试。
  • 数据库逻辑集中的系统需要更多集成测试。
  • 简单数据管道可以以组件测试为主,重点检查数据在组件间的转换。

4. 真实依赖​

验证数据访问行为时,应使用与生产兼容的真实数据库。Testcontainers 在测试中启动容器,让用例连接固定版本的依赖。下面用 PostgreSQL 容器举例,使用 Testcontainers 1.21.3 的 API:

@Testcontainers
class OrderRepositoryTest {
@Container
static PostgreSQLContainer<?> database =
new PostgreSQLContainer<>("postgres:18");
}

示例声明了 PostgreSQL 容器,测试框架会管理它的启动与停止。持续集成(CI)也能使用这个镜像建立隔离环境。为了让结果可重复,还需要处理五项环境约束:

  • 执行 schema 迁移,也就是按版本创建和更新表结构。
  • 清理每个用例写入的数据,防止后续用例读到残留记录。
  • 明确容器何时启动、是否复用,平衡启动成本与隔离程度。
  • 核对 ARM/x86 架构和生产插件的差异,确认测试覆盖实际部署环境。
  • 确认外部镜像能够获取,并检查镜像来源和供应链安全。

固定镜像提高了环境的可重复性,生产参数和数据规模仍需单独核对。

数据访问的具体测试范围见 批处理与分页。

5. 契约测试​

契约测试用可执行样例记录服务之间的接口约定。比如服务 A 调用服务 B,A 就是消费方,B 就是提供方。双方各自测试通过后,仍会因字段、状态码或默认值变化而集成失败。下面的契约样例记录了消费方依赖的支付结果:

{
"status": "PAID",
"amount": {
"value": "10.00",
"currency": "CNY"
}
}

样例约定了支付状态、金额和币种的表示方式。消费方验证客户端能处理该样例,提供方再验证当前实现能返回约定结果。这样可以发现以下接口兼容性问题:

  • 提供方删除或重命名了消费方需要的字段。
  • 字段类型或可空性改变,使客户端解析或校验失败。
  • 状态码或错误体改变,使客户端进入错误的处理分支。
  • 消费方依赖的枚举值改变,客户端无法按原约定处理状态。

契约验证覆盖约定的交互,真实网络容量和认证设施仍需集成测试。跨多个服务的完整业务结果,则交给少量端到端测试验证。

6. 回归测试​

回归测试用于防止已经修好的缺陷再次出现。修复 bug 时,先保留能稳定失败的最小用例,再修改实现。这个用例需要明确缺陷的触发条件与预期行为:

  • 记录触发问题的输入,让缺陷能够重现。
  • 写明必须保持的外部行为,让断言有明确依据。
  • 验证修复后的结果,防止后续变更再次破坏该行为。

缩小用例时,保留触发条件,删除与根因无关的生产请求字段和数据。这样,后来维护代码的人才能看懂测试为何失败。涉及真实集成路径的缺陷,还需在对应层级验证修复。

7. 覆盖率的含义​

覆盖率用来回答测试执行到了哪里。JaCoCo 把至少一条指令执行过的行标为已覆盖。分支计数覆盖 if 和 switch 的分支。异常处理不计入它的分支覆盖率。

行覆盖率达到 100%,仍会留下五类盲区:

  • 代码执行了,但断言没有检查关键结果。
  • 某行中的分支尚未走全,或分支全覆盖后仍遗漏输入组合。
  • 测试复制了实现中的错误算法,算出的期望值也错了。
  • 并发时序和外部协议边界尚未验证。
  • 异常被捕获后,测试没有检查数据或对象是否处于正确状态。

所以,覆盖率适合寻找未执行区域,质量判断还要结合这些盲区。边界用例和真实集成检查行为,变异测试进一步检查断言能否发现代码错误。具体要覆盖什么,仍由业务风险决定。

8. 不稳定测试​

不稳定测试也称为 flaky test,指代码未变时仍会偶发失败的测试。排查时可以检查以下六类干扰来源:

  • 用例依赖真实时间或固定 sleep,机器变慢后等待时间就不够。
  • 用例共享数据,结果随执行顺序改变。
  • 异步任务尚未结束,测试就开始断言。
  • 随机端口冲突、网络波动或第三方服务故障影响结果。
  • 并发断言缺少同步条件,读到的状态随线程调度改变。
  • 数据库清理或事务边界错误,使记录残留或提前回滚。

重跑可以收集失败频率与环境证据,但一次重跑通过并没有消除根因。持续误报会让团队忽略实际发生的回归,所以要修复共享状态、时间或等待条件。临时隔离时,应记录恢复条件并补足关键覆盖。已经失去验证价值的用例可以删除,其原本覆盖的风险仍需重新评估。

9. 风险与测试层级​

安排测试时,要把业务影响和验证成本一起考虑。可以用以下五个问题检查每项变更:

  1. 错误影响多大?
  2. 最便宜的哪一层能可靠发现它?
  3. 哪些真实组件行为不能被替身模拟?
  4. 哪个边界由其他团队或服务维护?
  5. 失败后能否快速定位和重放?

以支付适配器为例,可以按以下四个层级安排验证:

  • 单元测试验证金额处理、幂等键生成和错误映射。
  • 契约测试验证请求字段、签名输入和响应结构。
  • 集成测试验证 HTTP 客户端超时、TLS 连接与序列化。
  • 端到端测试在沙箱中各验证一条成功和失败的主流程。

10. 消费方契约与服务端验证​

消费方测试通过后,还需要确认服务端确实满足同一份契约。Pact 是实现这套流程的契约测试工具。它先让真实客户端请求模拟提供方,测试通过后生成交互契约。随后,提供方验证把同一交互回放给实际实现,检查响应是否符合约定。

每项交互需要独立的 provider state,也就是提供方处理请求前应具备的状态。例如先准备“订单存在”的数据,再验证查询响应。每项交互分别准备数据,就能避免依赖执行顺序。

只有模拟服务测试通过,还缺少提供方验证。发布检查应使用活跃消费者版本对应的契约结果。

11. 变异测试结果​

变异测试用于检查断言对代码错误是否敏感。PIT 是 Java 的变异测试工具,会临时改变条件、运算或返回值,再运行相关测试。测试因此失败,称为杀死该变异。如果测试仍通过,就称为变异存活。

分析报告时,需要区分三种情况:

  • 修改位置没有被测试执行到,应先检查覆盖范围。
  • 修改位置已执行,行为也变了,但测试仍通过,应补边界输入或结果断言。
  • 修改只改变代码形式,外部行为仍相同,这属于等价变异。

例如把金额校验 < 0 改成 <= 0。零金额合法的测试应失败,从而发现边界被改坏。

12. 常见问题​

12.1 有端到端测试还需要单元测试吗?​

需要,两者验证的范围不同。端到端测试证明少量完整路径能工作,但覆盖边界慢,失败时也难定位。单元测试能低成本覆盖规则边界和失败分支,所以适合与端到端测试组合使用。

12.2 H2 能替代目标数据库的集成测试吗?​

12.3 契约测试是否就是接口文档校验?​

12.4 测试中的重复代码要不要抽取?​

13 · 面试题卡片​

13.1 单元测试、集成测试和端到端测试分别能发现什么问题​

出现公司:招银网络、阿里巴巴、爱客科技

考察重点:解释各层使用哪些真实依赖,以及测试范围怎样影响失败定位成本。能按变更风险组合测试,并说明全部使用 Mock 或端到端测试会留下什么问题。

相关内容:测试范围、底层测试的数量、按风险分配测试层级。

参考回答

单元测试发现局部规则错误,集成测试发现组件协作错误,端到端测试发现完整业务路径中的错误。

单元测试运行快、定位清楚,适合覆盖金额计算等细分规则。集成测试连接真实数据库、消息系统或 HTTP 客户端,验证映射和配置。端到端测试在部署环境中检查关键流程,执行更慢,失败定位也更困难。

所以,应把用例放在能可靠发现问题的最小范围,再由更高层验证关键集成。跨服务的字段和响应兼容性还要用契约测试覆盖。

13.2 为什么高覆盖率仍然可能发生严重回归​

出现公司:腾讯、字节跳动、快手、滴滴

考察重点:区分执行过代码与验证了正确结果,解释输入组合、外部协议和并发时序会留下哪些盲区。能用覆盖率寻找缺失用例,并结合断言判断质量。

相关内容:回归测试、覆盖率的含义。

参考回答

高覆盖率仍会发生严重回归,因为代码执行过并不说明断言检查了正确结果。

行已执行后仍会遗漏分支,分支走全后仍会遗漏输入组合。测试若复制了实现中的错误算法,连期望值也会算错。

并发时序、外部协议和异常后的状态也需要专门用例。因此,要结合边界测试、真实集成、契约兼容和历史回归检查风险。变异测试能进一步检查断言是否敏感,具体测试范围仍由业务风险决定。

13.3 消费方对模拟服务测试通过,为什么还要验证提供方?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:解释契约怎样生成和回放,以及 provider state 怎样准备交互所需的数据。

相关内容:消费方契约与服务端验证。

参考回答

还要验证提供方,是因为模拟服务测试只证明客户端能够处理约定的交互。

模拟响应与实际实现可以不一致,所以要把同一契约回放给提供方。验证前,再用 provider state 为每项交互准备独立数据,避免结果依赖执行顺序。

13.4 变异测试中的存活变异一定说明测试有缺陷吗?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:判断存活变异是否改变了可观察行为,并据此区分边界或断言缺失与等价变异。

相关内容:变异测试结果。

参考回答

不一定,因为等价变异只改变代码形式,外部行为仍然相同。

分析时先确认修改是否改变了可观察行为。如果行为已经改变而测试仍通过,就要补充能识别差异的边界输入或结果断言。

14. 参考资料​