测试金字塔、契约与回归成本
不同测试提供不同证据:单元测试验证局部规则,集成测试验证真实组件协作,契约测试验证服务边界,端到端测试验证少量关键流程。测试组合应按风险分层,而不是追求某一种测试覆盖全部系统。
1. 测试范围决定能证明什么
| 类型 | 主要证据 | 常见盲区 |
|---|---|---|
| 单元测试 | 一个行为单元的规则与边界 | 框架配置、真实协议、数据库 |
| 组件或切片测试 | Web、数据访问等局部集成 | 完整部署与跨服务链路 |
| 集成测试 | 真实数据库、消息或客户端协作 | 全系统用户流程 |
| 契约测试 | 提供方与消费方接口兼容 | 服务内部正确性、真实容量 |
| 端到端测试 | 部署环境中的关键业务路径 | 细粒度失败定位、所有组合 |
同一个故障可能需要多层防线。金额舍入规则用单元测试最直接;数据库列精度需要集成测试;支付回调字段兼容需要契约测试;真实下单主路径用少量端到端测试。
2. 为什么底层测试通常更多
单元测试运行快、失败定位清楚,适合覆盖大量分支和边界。端到端测试跨进程、网络、数据和异步任务:
- 环境更难建立。
- 运行时间更长。
- 失败原因更多。
- 测试数据更易冲突。
- 重试可能掩盖真实不稳定。
因此常见形状是底层测试多、越往上数量越少。它不是固定比例;数据库重逻辑系统会有更多集成测试,简单数据管道可能以组件测试为主。
3. 真实依赖要可重复建立
数据访问测试应使用与生产兼容的真实数据库,而不是只用行为不同的内存替代:
@Testcontainers
class OrderRepositoryTest {
@Container
static PostgreSQLContainer<?> database =
new PostgreSQLContainer<>("postgres:18");
}
容器化依赖可以固定镜像版本,并为 CI 创建隔离环境。测试仍要管理:
- schema 迁移。
- 每个用例的数据清理。
- 启动和复用策略。
- ARM/x86 与生产插件差异。
- 外部镜像获取和供应链安全。
Testcontainers 提高可重复性,不代表容器环境与生产完全相同。
4. 契约测试检查服务边界
服务 A 调用服务 B 时,双方可能各自测试通过,却因字段、状态码或默认值变化在集成时失败。契约测试记录消费方真实依赖:
{
"status": "PAID",
"amount": {
"value": "10.00",
"currency": "CNY"
}
}
消费方验证客户端能处理契约样例,提供方验证当前实现能满足该契约。它可以发现:
- 删除或重命名字段。
- 类型与可空性变化。
- 状态码和错误体变化。
- 消费方依赖的枚举值变化。
契约测试不验证真实网络容量、认证基础设施和长链路业务结果,仍需少量集成与端到端验证。
5. 回归测试来自真实缺陷
修复 bug 时先保留能稳定失败的最小用例,再修改实现。这个用例记录的是:
- 哪个输入触发问题。
- 哪条外部行为必须保持。
- 修复后未来变更不能再次破坏什么。
不要把完整生产请求和大量无关数据原样复制成一个难以理解的测试。缩小到根因,同时保留一次更高层测试证明真实集成路径。
6. 覆盖率只说明代码被执行
行覆盖率 100% 仍可能:
- 没有断言关键结果。
- 所有分支都走过,但组合未覆盖。
- 测试重复实现了同一个错误。
- 并发、时序和协议边界没有验证。
- 异常被捕获后没有检查状态。
覆盖率适合发现“完全没跑到”的区域,不适合作为质量结论。还可以使用 mutation testing 临时改变条件和返回值,检查测试是否能杀死错误,但它同样不能替代领域风险分析。
7. 测试不稳定会增加回归成本
常见 flaky 来源:
- 依赖真实时间和 sleep。
- 测试执行顺序和共享数据。
- 未等待的异步任务。
- 随机端口、网络和第三方服务。
- 并发断言没有同步条件。
- 数据库清理与事务边界错误。
不应长期通过自动重跑把失败变绿。重跑可以收集证据,但 flaky 测试要被修复、隔离或删除。持续误报会让团队忽略真正回归。
8. 按风险分配测试层级
可以对每项变化问:
- 错误影响多大?
- 最便宜的哪一层能可靠发现它?
- 哪些真实组件行为不能被替身模拟?
- 哪个边界由其他团队或服务维护?
- 失败后能否快速定位和重放?
例如支付适配器:
- 单元:金额、幂等键和错误映射。
- 契约:请求字段、签名输入和响应结构。
- 集成:HTTP 客户端超时、TLS 与序列化。
- 端到端:沙箱中一条成功和一条失败主流程。
9. 常见问题
9.1 有端到端测试还需要单元测试吗
需要。端到端证明少量真实路径能工作,但覆盖边界慢、定位困难;单元测试能低成本覆盖领域规则和失败分支。两者证据范围不同。
9.2 H2 能替代 MySQL 或 PostgreSQL 集成测试吗
只能覆盖与它共同支持的部分 SQL 和 JDBC 逻辑。方言、锁、索引、时间类型和约束行为可能不同。生产依赖的数据库语义应在同类真实数据库上验证。
9.3 契约测试是否等于接口文档校验
不等于。文档描述期望,契约测试由可执行样例验证消费方依赖和提供方实现是否兼容。仍需治理契约版本和失效消费方。
9.4 测试重复代码要不要抽取
重复的环境建立和对象工厂可以抽取,业务场景的关键输入应保持可见。过度抽象会让测试失败时必须跳转多层才能理解。
10. 面试题
10.1 单元测试、集成测试和端到端测试分别能发现什么问题
出现公司:招银网络、阿里巴巴、爱客科技
考察重点
- 每层的真实依赖范围和失败定位成本。
- 为什么不能全部 Mock 或全部端到端。
- 怎样按变更风险组合证据。
相关内容:第 1 节“测试范围决定能证明什么”、第 2 节“为什么底层测试通常更多”、第 8 节“按风险分配测试层级”。
参考回答
单元测试快速验证局部业务规则和边界,不能证明框架与真实基础设施;集成测试使用真实数据库、消息或 HTTP 客户端验证映射和配置;端到端测试在部署环境验证少量关键业务链路,但慢且失败定位困难。
测试组合按风险设计:大部分细分规则在低层覆盖,真实组件行为由集成测试确认,跨服务接口用契约测试,只有关键主路径保留端到端测试。没有一种层级能单独证明全部系统。
10.2 为什么高覆盖率仍然可能发生严重回归
出现公司:腾讯、字节跳动、快手、滴滴
考察重点
- 执行过代码与验证正确结果的区别。
- 组合、协议、并发和时序盲区。
- 覆盖率怎样作为诊断指标而非目标。
相关内容:第 5 节“回归测试来自真实缺陷”、第 6 节“覆盖率只说明代码被执行”。
参考回答
覆盖率只证明测试执行过某些行或分支,不证明断言能识别错误,也不覆盖所有输入组合、并发时序和外部协议。测试甚至可能复制生产代码的同一种错误。
可以用覆盖率找到未执行区域,但质量还要看边界用例、真实依赖集成、契约兼容和历史缺陷回归。对关键模块可补 mutation testing 检查断言敏感度,但仍要从业务风险选择测试。