这是《Spring Boot 与 Spring Cloud 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 测试的目标是用可重复的方式证明行为正确,并在修改时及时暴露回归。Spring 项目测试的关键是分层:单元测试快而稳定,集成测试验证装配和外部契约,端到端测试少量覆盖关键链路。
14.1 测试金字塔
E2E
/ \
API / 集成
/ \
单元测试
| 层 | 目标 | 速度 |
|---|---|---|
| 单元测试 | 业务逻辑和领域规则 | 快 |
| 切片测试 | Web、Data、JSON 等局部 | 较快 |
| 集成测试 | 容器装配、数据库、消息 | 慢 |
| E2E | 用户关键路径 | 最慢 |
数量建议:单元测试最多,集成测试适量,E2E 少而稳定。
14.2 测试依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<scope>test</scope>
</dependency>
常用库:
| 库 | 用途 |
|---|---|
| JUnit 5 | 测试框架 |
| AssertJ | 断言 |
| Mockito | 替身 |
| JSONPath | JSON 断言 |
| Testcontainers | 真实依赖容器 |
| Awaitility | 异步断言 |
| ArchUnit | 架构约束 |
14.3 单元测试
被测类:
public class DiscountCalculator {
public BigDecimal calculate(UserLevel level, BigDecimal amount) {
return switch (level) {
case VIP -> amount.multiply(new BigDecimal("0.85"));
case NORMAL -> amount;
};
}
}
测试:
class DiscountCalculatorTest {
@Test
void should_apply_vip_discount() {
DiscountCalculator calculator = new DiscountCalculator();
BigDecimal result = calculator.calculate(UserLevel.VIP, new BigDecimal("100.00"));
assertThat(result).isEqualByComparingTo("85.00");
}
}
单元测试不需要启动 Spring。大量 @SpringBootTest 会拖慢构建,也会让失败定位变模糊。
14.4 Mockito
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
OrderRepository repository;
@Test
void should_create_order() {
when(repository.nextId()).thenReturn(1L);
OrderService service = new OrderService(repository);
Order order = service.create(new CreateOrderCommand("u1", 2, 1));
assertThat(order.status()).isEqualTo(OrderStatus.CREATED);
verify(repository).save(any(Order.class));
}
}
原则:
- 不 mock 值对象;
- 不过度验证所有调用;
- 测试行为而不是实现细节;
- 不用 mock 代替设计问题;
- 异步逻辑用 Awaitility 验证。
14.5 Web 切片测试
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
OrderService orderService;
@Test
void should_return_order() throws Exception {
when(orderService.get(1L)).thenReturn(new OrderView(1L, "CREATED"));
mockMvc.perform(get("/api/orders/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.status").value("CREATED"));
}
}
切片测试只加载 Web 相关 Bean,比完整启动更快,适合验证:
- 参数绑定;
- 校验;
- 状态码;
- JSON 序列化;
- 异常处理;
- 安全规则。
Spring Boot 新版本对 mock Bean 注解有调整,以当前测试 API 为准。
14.6 Testcontainers
@SpringBootTest
@Testcontainers
class OrderIntegrationTests {
@Container
@ServiceConnection
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.4");
@Autowired
TestRestTemplate restTemplate;
@Test
void should_create_and_query_order() {
OrderView created = restTemplate.postForObject(
"/api/orders", new CreateOrderRequest("u1", 1L, 1), OrderView.class);
assertThat(created).isNotNull();
}
}
优点:
- 使用真实数据库;
- 避免 H2 与 MySQL 方言差异;
- 验证 SQL 和事务;
- 可复现环境。
注意:
- 本机需要容器运行时;
- CI 需要缓存镜像;
- 容器启动有成本;
- 测试数据要隔离;
- 不应每个方法都重建容器。
14.7 数据库测试
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class OrderRepositoryTests {
@Container
@ServiceConnection
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.4");
@Autowired
OrderRepository repository;
@Test
void should_find_by_user() {
repository.save(new OrderEntity("u1", OrderStatus.CREATED));
List<OrderEntity> orders = repository.findByUserId("u1");
assertThat(orders).hasSize(1);
}
}
事务回滚策略:
| 注解 | 默认行为 |
|---|---|
@DataJpaTest |
每个测试后回滚 |
@SpringBootTest |
不自动回滚 |
共享容器的集成测试要显式清理数据,避免测试顺序耦合。
14.8 契约测试
生产者验证契约:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProducerContractTests {
}
消费者使用 mock server 验证:
@AutoConfigureMockMvc
class ConsumerTests {
}
Spring Cloud Contract 的价值在于:
- 服务双方共享契约;
- 提前发现接口破坏;
- 消费者测试不依赖生产者部署;
- CI 阻止不兼容变更。
契约测试不能替代少量 E2E,但能显著减少跨服务联调成本。
14.9 测试配置
测试配置文件:
spring:
datasource:
url: jdbc:tc:mysql:8.4:///order
Profile:
@ActiveProfiles("test")
建议:
- 不访问真实生产资源;
- 不依赖外部服务状态;
- 测试数据可重复构造;
- 不依赖执行顺序;
- 时间、随机数可注入;
- 网络调用使用 WireMock 或容器;
- 敏感信息不写入测试代码。
14.10 CI 与质量门禁
Maven:
mvn test
mvn verify
Gradle:
gradle test
gradle check
门禁建议:
| 指标 | 建议 |
|---|---|
| 单元测试成功率 | 100% |
| 核心业务行覆盖 | 按模块设定 |
| 集成测试 | 关键路径必跑 |
| 契约变更 | 双方确认 |
| ArchUnit | 架构规则 |
| 依赖漏洞 | 自动扫描 |
| 测试耗时 | 设置预算 |
覆盖率是结果指标,不是目标。为了覆盖率写无意义测试反而降低质量。
本章小结
Spring 测试应分层:领域和 Service 用快速单元测试,Web/Data 用切片测试,容器装配和外部依赖用 Testcontainers 集成测试,服务间契约用契约测试,端到端只保留关键路径。测试要稳定、可重复、与生产环境相似,并纳入 CI 门禁。
思考题
- 什么时候不应使用
@SpringBootTest? - Mock 和 Testcontainers 各解决什么问题?
@DataJpaTest的默认事务行为是什么?- 契约测试解决微服务什么痛点?
- 如何保持 CI 测试速度快?