4、集成测试策略:Spring Boot Test、Testcontainers(数据库/消息队列)、@SpringBootTest与@WebMvcTest
好,咱们进入第四章。这一章聊的是集成测试策略,说白了就是——你的微服务跟别人(数据库、消息队列、其他服务)能不能好好配合。
单元测试我们测的是“我自己的逻辑对不对”,集成测试要回答的是“我跟外部组件连上后,还能不能正常工作”。我个人习惯,集成测试是微服务质量保障的底线,绝对不能跳过。
4.1 为什么集成测试这么重要?
你想想看,一个微服务里,最常出问题的地方在哪?
- SQL写错了,字段名对不上
- Redis缓存序列化失败
- 消息队列发送后消费端没收到
- Feign调用时参数传错了
这些问题,单元测试根本发现不了。我在项目中遇到过好几次,单元测试全绿,一部署到测试环境就崩。后来我定了个规矩:所有涉及外部组件的代码,必须有集成测试覆盖。
核心原则:集成测试不是“可选项”,而是“必选项”。它验证的是你的代码与外部依赖之间的契约是否成立。
4.2 Spring Boot Test 全家桶
Spring Boot 给我们提供了一套非常完善的测试工具。我把它分成三个层次:
| 注解 | 加载范围 | 适用场景 |
|---|---|---|
| @WebMvcTest | 仅Web层(Controller) | 测试接口请求/响应、参数校验 |
| @DataJpaTest | 仅JPA层(Repository) | 测试数据库访问、自定义查询 |
| @SpringBootTest | 完整应用上下文 | 端到端集成测试 |
嗯,这里要注意:不要一上来就用 @SpringBootTest。它启动整个Spring容器,慢得很。我见过一个项目,跑一次集成测试要20分钟,开发人员直接放弃了。
4.3 @WebMvcTest:轻量级Web层测试
@WebMvcTest 只加载Controller层相关的Bean,Service、Repository都不会加载。说白了,它只测“请求进来→参数解析→调用Service→返回结果”这一段。
我曾经用它抓到一个很隐蔽的问题:某个接口的@Valid校验没生效,因为参数对象里忘了加@NotNull注解。单元测试测不到,但@WebMvcTest一跑就暴露了。
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void shouldReturnUserWhenIdExists() throws Exception {
// 模拟Service层行为
given(userService.findById(1L))
.willReturn(new User(1L, "张三"));
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("张三"));
}
}
我的建议:每个Controller都写一个@WebMvcTest。它跑得快(几秒钟),能覆盖接口契约、参数校验、异常处理。性价比极高。
4.4 @SpringBootTest:全量集成测试
@SpringBootTest 会启动完整的Spring应用上下文。它适合做端到端的场景验证,比如:
- 用户注册→发送消息→消费消息→更新状态
- 订单创建→扣减库存→生成支付记录
但要注意,它默认会加载所有Bean,包括那些需要外部依赖的(比如数据库、MQ)。所以我们需要配合 Testcontainers 来管理这些外部组件。
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldCreateOrderSuccessfully() {
// 真实调用,走完整链路
ResponseEntity<Order> response = restTemplate.postForEntity(
"/orders",
new CreateOrderRequest("商品A", 2),
Order.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().getStatus()).isEqualTo("PAYMENT_PENDING");
}
}
避坑指南:我曾经因为@SpringBootTest加载了太多Bean,导致测试启动花了3分钟。后来我用了@SpringBootTest(classes = {核心配置类.class})来缩小加载范围。记住:只加载你需要的配置。
4.5 Testcontainers:让外部依赖“随测随走”
Testcontainers 是我这几年最喜欢的测试工具之一。它能在测试时启动Docker容器,测试结束后自动销毁。完美解决了“测试环境依赖”的问题。
为什么不用H2内存数据库?嗯,这个问题我回答过很多次了:H2的SQL方言和真实数据库不一样。我在项目中遇到过,H2上跑得好好的SQL,部署到MySQL就报语法错误。所以,我坚持用Testcontainers启动真实数据库。
4.5.1 数据库集成测试
@SpringBootTest
@Testcontainers
class UserRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void shouldSaveAndFindUser() {
User user = new User("李四", "lisi@example.com");
userRepository.save(user);
Optional<User> found = userRepository.findByEmail("lisi@example.com");
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("李四");
}
}
关键点:@DynamicPropertySource 会在运行时动态覆盖配置。这样你的代码里不需要写死任何测试环境的连接信息。
4.5.2 消息队列集成测试
消息队列的集成测试更复杂一些。你需要:
- 启动Kafka/RabbitMQ容器
- 发送消息
- 等待消费端处理
- 验证结果
我记得有一次,我们团队在测试Kafka消息重试机制时,发现消费端一直报反序列化错误。用Testcontainers一测,原来是消息体里多了一个字段,生产者和消费者的Schema不一致。
@SpringBootTest
@Testcontainers
class OrderMessageIntegrationTest {
@Container
static KafkaContainer kafka = new KafkaContainer(
DockerImageName.parse("confluentinc/cp-kafka:7.4.0")
);
@DynamicPropertySource
static void configureKafka(DynamicPropertyRegistry registry) {
registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
}
@Autowired
private KafkaTemplate<String, OrderEvent> kafkaTemplate;
@Autowired
private OrderEventConsumer consumer;
@Test
void shouldConsumeOrderCreatedEvent() throws Exception {
OrderEvent event = new OrderEvent("ORDER-001", "CREATED");
kafkaTemplate.send("order-events", event);
// 等待消费端处理(异步)
await().atMost(10, TimeUnit.SECONDS)
.until(() -> consumer.getProcessedEvents().size() == 1);
assertThat(consumer.getProcessedEvents().get(0).getOrderId())
.isEqualTo("ORDER-001");
}
}
个人经验:消息队列的测试一定要加超时等待。我见过太多测试因为异步处理没来得及完成就断言,导致测试不稳定。用Awaitility库可以优雅地解决这个问题。
4.6 集成测试的最佳实践
最后,我总结几条实战中沉淀下来的经验:
- 分层测试:能用@WebMvcTest就别用@SpringBootTest,能用一个容器就别启动两个
- 隔离性:每个测试类用独立的数据库/队列,避免测试间相互影响
- 速度优先:集成测试跑得慢是原罪。我一般把集成测试单独放在一个Maven profile里,CI/CD中才执行
- 失败即停止:集成测试一旦失败,立即排查。它暴露的问题往往比单元测试严重得多
好了,这一章的内容就到这里。下一章我们会聊契约测试,那是微服务间接口测试的利器。到时候见。