单元测试、集成测试、Testcontainers与质量保障
前言
自动化测试的目标是让业务变更能够快速验证并安全交付,而不是单纯追求覆盖率数字。本文按测试金字塔梳理单元、集成、契约和端到端测试,并说明 Testcontainers、测试数据隔离和 CI 质量门禁在生产研发流程中的用法。
测试目标是降低变更风险
高覆盖率不等于高质量。测试应快速发现业务规则回归、接口契约破坏、数据库/MQ/缓存集成问题和发布环境差异。健康的结构通常是大量快速单元测试,适量集成测试,少量关键端到端测试,而不是所有测试都启动完整 Spring 容器。
| 层次 | 验证内容 | 成本 | 典型工具 |
|---|---|---|---|
| 单元测试 | 单个类的规则、边界、异常 | 最低、最快 | JUnit 5、Mockito |
| Slice 测试 | MVC/JPA 等局部 Spring 层 | 中等 | @WebMvcTest、@DataJpaTest |
| 集成测试 | 真实数据库、Redis、MQ、事务和配置 | 较高 | @SpringBootTest、Testcontainers |
| 契约/E2E | 服务间 API 契约、关键用户路径 | 最高、最慢 | Pact、WireMock、Playwright/API tests |
JUnit 5 与可维护单测
JUnit 5 使用 @Test、@ParameterizedTest、@Nested、@BeforeEach 等组织测试。测试名应描述业务行为,例如 shouldRejectRefundWhenOrderIsAlreadyClosed,而不是 test1。每个测试只验证一个清晰规则,避免依赖执行顺序和共享可变静态数据。
1 |
|
时间、随机数、ID 生成和远程依赖应通过接口或 Clock 注入,以便测试可控。不要在单测中使用 Thread.sleep() 等待异步结果;应使用可控制的 executor、latch 或将异步边界抽象出来。
Mockito 的正确边界
Mockito 适合替换网络、数据库网关、支付 SDK 等外部协作者,验证当前类如何处理它们。不要 mock 被测对象本身,也不要为了断言内部私有调用顺序写大量脆弱 mock。
1 | when(stockGateway.reserve("sku-1", 2)).thenReturn(Reservation.success("r-1")); |
Mock 返回的是测试设定的行为,不能证明真实 SQL、序列化、事务或 HTTP 协议正确。因此 Mock 单测必须由集成测试补充。对第三方 HTTP 可用 WireMock/MockWebServer 验证请求格式和错误响应,但关键兼容性仍应在真实或契约环境验证。
Spring 测试与 Testcontainers
@SpringBootTest 会启动完整应用上下文,适合验证 Bean 装配、事务、真实链路,但启动慢、上下文污染概率高。Controller 层优先使用 @WebMvcTest,Repository 层使用 @DataJpaTest,仅加载需要的组件。
Testcontainers 在测试时启动真实 MySQL、PostgreSQL、Redis、Kafka 等容器,避免 H2 与生产数据库 SQL 方言、事务、索引行为不一致。容器测试应固定镜像版本、控制初始化数据、在 CI 可用 Docker 环境执行,并按测试套件共享或复用容器以控制耗时。
1 |
|
示例只表示方向,实际项目应通过 @DynamicPropertySource 或框架集成将容器地址注入应用配置。测试不能连接开发/生产共享库;每个测试应创建独立数据、使用事务回滚或在 teardown 清理,并保证并行执行不相互覆盖。
异步、消息与契约测试
消息消费测试至少覆盖重复投递、乱序/迟到、失败重试、死信和幂等。不要只断言“发送方法被调用”,还要验证事件 ID、业务主键、版本、消费后的数据库状态和重复消费结果。Outbox 模式要分别测本地事务写入和发布器重试。
服务间 API 变化应有 OpenAPI/schema 或消费者驱动契约。兼容性规则包括:新增响应字段通常兼容,删除或改类型通常不兼容;枚举新增值也会让严格客户端失败。契约测试不能替代灰度和监控,但能把一部分跨团队回归前移到 CI。
CI 质量门禁
质量门禁应分层执行:PR 先运行格式化、静态检查、单元测试;合并后运行集成/容器测试;发布前运行迁移、契约和关键 E2E。失败测试必须可重现,偶发测试(flaky test)不是“重跑就好”,它会降低团队对 CI 的信任,应记录环境、随机种子、时间依赖和并发条件后修复。
覆盖率用于发现未测试区域,不应成为唯一 KPI。更有效的指标包括关键业务规则覆盖、缺陷逃逸率、构建时长、失败重跑率、变更后的回滚率。支付、库存、权限和数据迁移等高风险模块可增加 mutation testing 或针对边界场景的性质测试。
高频面试题
| 问题 | 回答要点 |
|---|---|
为什么不全用 @SpringBootTest? |
全量上下文慢、耦合强、定位差;优先单测和 slice test,关键链路才做集成测试。 |
| Mock 能证明数据库逻辑吗? | 不能。Mock 验证协作,真实 SQL、事务和方言要由集成测试验证。 |
| 为什么用 Testcontainers? | 用接近生产的真实依赖替代内存模拟,发现 SQL、配置和协议差异。 |
| 如何测试消息幂等? | 用相同事件 ID 重复消费,断言业务状态/流水只产生一次,并覆盖失败重试。 |
| 覆盖率 100% 是否够? | 不够,可能只覆盖实现路径而未验证业务断言和边界;应关注风险与契约。 |


