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 消息队列集成测试

消息队列的集成测试更复杂一些。你需要:

  1. 启动Kafka/RabbitMQ容器
  2. 发送消息
  3. 等待消费端处理
  4. 验证结果

我记得有一次,我们团队在测试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中才执行
  • 失败即停止:集成测试一旦失败,立即排查。它暴露的问题往往比单元测试严重得多

好了,这一章的内容就到这里。下一章我们会聊契约测试,那是微服务间接口测试的利器。到时候见。