测试金字塔
测试金字塔按验证范围组织测试,让不同层级分别发现局部规则和组件协作中的错误。本文先介绍各层测试的范围与成本,再说明真实依赖和契约怎样验证。最后讨论回归测试、覆盖率和不稳定测试,帮助你按业务风险安排测试。
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. 风险与测试层级
安排测试时,要把业务影响和验证成本一起考虑。可以用以下五个问题检查每项变更:
- 错误影响多大?
- 最便宜的哪一层能可靠发现它?
- 哪些真实组件行为不能被替身模拟?
- 哪个边界由其他团队或服务维护?
- 失败后能否快速定位和重放?
以支付适配器为例,可以按以下四个层级安排验证:
- 单元测试验证金额处理、幂等键生成和错误映射。
- 契约测试验证请求字段、签名输入和响应结构。
- 集成测试验证 HTTP 客户端超时、TLS 连接与序列化。
- 端到端测试在沙箱中各验证一条成功和失败的主流程。
10. 消费方契约与服务端验证
消费方测试通过后,还需要确认服务端确实满足同一份契约。Pact 是实现这套流程的契约测试工具。它先让真实客户端请求模拟提供方,测试通过后生成交互契约。随后,提供方验证把同一交互回放给实际实现,检查响应是否符合约定。
每项交互需要独立的 provider state,也就是提供方处理请求前应具备的状态。例如先准备“订单存在”的数据,再验证查询响应。每项交互分别准备数据,就能避免依赖执行顺序。
只有模拟服务测试通过,还缺少提供方验证。发布检查应使用活跃消费者版本对应的契约结果。
11. 变异测试结果
变异测试用于检查断言对代码错误是否敏感。PIT 是 Java 的变异测试工具,会临时改变条件、运算或返回值,再运行相关测试。测试因此失败,称为杀死该变异。如果测试仍通过,就称为变异存活。
分析报告时,需要区分三种情况:
- 修改位置没有被测试执行到,应先检查覆盖范围。
- 修改位置已执行,行为也变了,但测试仍通过,应补边界输入或结果断言。
- 修改只改变代码形式,外部行为仍相同,这属于等价变异。
例如把金额校验 < 0 改成 <= 0。零金额合法的测试应失败,从而发现边界被改坏。
12. 常见问题
12.1 有端到端测试还需要单元测试吗?
需要,两者验证的范围不同。端到端测试证明少量完整路径能工作,但覆盖边界慢,失败时也难定位。单元测试能低成本覆盖规则边界和失败分支,所以适合与端到端测试组合使用。
12.2 H2 能替代目标数据库的集成测试吗?
H2 可以验证它与目标数据库共同支持的 SQL 和 JDBC 逻辑。生产数据库特有的方言、锁、索引、时间类型和约束行为,仍需在同类真实数据库中验证。否则,H2 中通过的用例会漏掉生产环境的行为差异。
12.3 契约测试是否就是接口文档校验?
两者验证的对象不同。接口文档描述预期,契约测试执行消费方交互,再验证提供方实现。因此,契约版本要跟随活跃消费方维护,已经停用的消费方也要及时退出验证。
12.4 测试中的重复代码要不要抽取?
重复的环境建立和对象工厂可以抽取。关键输入与期望结果应保留在用例附近,让读者直接看懂业务场景。如果理解一次失败需要跳转多层辅助方法,就应减少这类封装。
13 · 面试题卡片
13.1 单元测试、集成测试和端到端测试分别能发现什么问题
出现公司:招银网络、阿里巴巴、爱客科技
考察重点:解释各层使用哪些真实依赖,以及测试范围怎样影响失败定位成本。能按变更风险组合测试,并说明全部使用 Mock 或端到端测试会留下什么问题。
参考回答
单元测试发现局部规则错误,集成测试发现组件协作错误,端到端测试发现完整业务路径中的错误。
单元测试运行快、定位清楚,适合覆盖金额计算等细分规则。集成测试连接真实数据库、消息系统或 HTTP 客户端,验证映射和配置。端到端测试在部署环境中检查关键流程,执行更慢,失败定位也更困难。
所以,应把用例放在能可靠发现问题的最小范围,再由更高层验证关键集成。跨服务的字段和响应兼容性还要用契约测试覆盖。
13.2 为什么高覆盖率仍然可能发生严重回归
出现公司:腾讯、字节跳动、快手、滴滴
考察重点:区分执行过代码与验证了正确结果,解释输入组合、外部协议和并发时序会留下哪些盲区。能用覆盖率寻找缺失用例,并结合断言判断质量。
参考回答
高覆盖率仍会发生严重回归,因为代码执行过并不说明断言检查了正确结果。
行已执行后仍会遗漏分支,分支走全后仍会遗漏输入组合。测试若复制了实现中的错误算法,连期望值也会算错。
并发时序、外部协议和异常后的状态也需要专门用例。因此,要结合边界测试、真实集成、契约兼容和历史回归检查风险。变异测试能进一步检查断言是否敏感,具体测试范围仍由业务风险决定。
13.3 消费方对模拟服务测试通过,为什么还要验证提供方?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:解释契约怎样生成和回放,以及 provider state 怎样准备交互所需的数据。
相关内容:消费方契约与服务端验证。
参考回答
还要验证提供方,是因为模拟服务测试只证明客户端能够处理约定的交互。
模拟响应与实际实现可以不一致,所以要把同一契约回放给提供方。验证前,再用 provider state 为每项交互准备独立数据,避免结果依赖执行顺序。
13.4 变异测试中的存活变异一定说明测试有缺陷吗?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:判断存活变异是否改变了可观察行为,并据此区分边界或断言缺失与等价变异。
相关内容:变异测试结果。
参考回答
不一定,因为等价变异只改变代码形式,外部行为仍然相同。
分析时先确认修改是否改变了可观察行为。如果行为已经改变而测试仍通过,就要补充能识别差异的边界输入或结果断言。