前言

自动化测试的目标是让业务变更能够快速验证并安全交付,而不是单纯追求覆盖率数字。本文按测试金字塔梳理单元、集成、契约和端到端测试,并说明 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
2
3
4
5
6
@ParameterizedTest
@ValueSource(longs = {0, -1})
void shouldRejectNonPositiveQuantity(long quantity) {
assertThrows(IllegalArgumentException.class,
() -> orderItem.of("sku-1", quantity));
}

时间、随机数、ID 生成和远程依赖应通过接口或 Clock 注入,以便测试可控。不要在单测中使用 Thread.sleep() 等待异步结果;应使用可控制的 executor、latch 或将异步边界抽象出来。

Mockito 的正确边界

Mockito 适合替换网络、数据库网关、支付 SDK 等外部协作者,验证当前类如何处理它们。不要 mock 被测对象本身,也不要为了断言内部私有调用顺序写大量脆弱 mock。

1
2
3
4
5
6
when(stockGateway.reserve("sku-1", 2)).thenReturn(Reservation.success("r-1"));

Order order = service.create(command);

assertThat(order.status()).isEqualTo(OrderStatus.CREATED);
verify(outbox).append(argThat(event -> event.orderId().equals(order.id())));

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
2
3
4
5
6
7
@Testcontainers
@SpringBootTest
class OrderRepositoryIT {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine");
}

示例只表示方向,实际项目应通过 @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% 是否够? 不够,可能只覆盖实现路径而未验证业务断言和边界;应关注风险与契约。