1. 概述

本文将介绍 Spring Boot 3.1 中新增的 Testcontainers 增强支持功能。这次更新提供了更简洁的容器配置方式,并支持在本地开发时启动容器。使用 Testcontainers 进行开发和测试将变得更加无缝高效。

2. SpringBoot 3.1 之前的 Testcontainers 用法

Testcontainers 能在测试阶段创建类生产环境,从而避免使用 Mock,编写出与实现细节解耦的高质量自动化测试。

本文示例使用一个简单的 Web 应用,包含 MongoDB 持久化层和 REST 接口:

@RestController
@RequestMapping("characters")
public class MiddleEarthCharactersController {
    private final MiddleEarthCharactersRepository repository;

    // 构造方法省略

    @GetMapping
    public List<MiddleEarthCharacter> findByRace(@RequestParam String race) {
        return repository.findAllByRace(race);
    }

    @PostMapping
    public MiddleEarthCharacter save(@RequestBody MiddleEarthCharacter character) {
        return repository.save(character);
    }
}

集成测试中,我们会启动包含数据库的 Docker 容器。由于容器暴露的端口是动态分配的,无法在配置文件中硬编码数据库 URL。因此,在 3.1 之前的 Spring Boot 版本中,需要使用 @DynamicPropertySource 注解将动态属性注册到 DynamicPropertyRegistry

@Testcontainers
@SpringBootTest(webEnvironment = DEFINED_PORT)
class DynamicPropertiesIntegrationTest {
    
    @Container
    static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"));

    @DynamicPropertySource 
    static void setProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl);
    }

    // ...
}

集成测试使用 @SpringBootTest 启动应用,并用 Testcontainers 搭建环境。最后使用 REST-assured 执行 HTTP 请求并验证响应:

@Test
void whenRequestingHobbits_thenReturnFrodoAndSam() {
    repository.saveAll(List.of(
        new MiddleEarthCharacter("Frodo", "hobbit"),
        new MiddleEarthCharacter("Samwise", "hobbit"),
        new MiddleEarthCharacter("Aragon", "human"),
        new MiddleEarthCharacter("Gandalf", "wizzard")
    ));

    when().get("/characters?race=hobbit")
      .then().statusCode(200)
      .and().body("name", hasItems("Frodo", "Samwise"));
}

3. 使用 @ServiceConnection 简化动态属性配置

从 SpringBoot 3.1 开始,可以使用 @ServiceConnection 注解消除动态属性配置的样板代码。

首先在 pom.xml 添加 spring-boot-testcontainers 依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-testcontainers</artifactId>
  <scope>test</scope>
</dependency>

然后移除注册动态属性的静态方法,直接在容器上添加 @ServiceConnection 注解:

@Testcontainers
@SpringBootTest(webEnvironment = DEFINED_PORT)
class ServiceConnectionIntegrationTest {

    @Container
    @ServiceConnection
    static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"));

    // ...

}

@ServiceConnection 让 Spring Boot 自动配置动态注册所需属性。其内部通过容器类或 Docker 镜像名自动确定需要配置的属性。

支持该注解的容器和镜像列表见 Spring Boot 官方文档

4. 本地开发中的 Testcontainers 支持

另一个激动人心的功能是 Testcontainers 能无缝集成到本地开发环境,只需少量配置。 这使得我们不仅在测试阶段,在本地开发时也能复现生产环境。

启用该功能需先创建 @TestConfiguration,将所有 Testcontainers 声明为 Spring Bean,并添加 @ServiceConnection 注解:

@TestConfiguration(proxyBeanMethods = false)
class LocalDevTestcontainersConfig {
    @Bean
    @ServiceConnection
    public MongoDBContainer mongoDBContainer() {
        return new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"));
    }
}

由于 Testcontainers 依赖通常是 test 作用域,需从 test 包启动应用。在 test 包中创建调用主 main() 方法的启动类:

public class LocalDevApplication {    
    public static void main(String[] args) {
        SpringApplication.from(Application::main)
          .with(LocalDevTestcontainersConfig.class)
          .run(args);
    }
}

现在通过该 main() 方法启动应用,它会自动使用 MongoDB 数据库。用 Postman 发送 POST 请求后,可直接连接数据库验证数据持久化:

postman post data

连接数据库需先获取容器暴露的端口,可通过应用日志或 docker ps 命令查看:

visual diff on changed files

最后用 MongoDB 客户端通过 URL mongodb://localhost:63437/test 连接,查询 characters 集合:

mongodb find all

 

成功连接并查询到本地开发环境 Testcontainers 启动的数据库数据。

5. 集成 DevTools 和 @RestartScope

本地开发时频繁重启应用会导致容器反复重启,但通过 Testcontainers 与 spring-boot-devtools 的集成,可以在应用重载时保持容器存活。 这个实验性功能能提升开发效率,节省时间并保留测试数据。

先添加 spring-boot-devtools 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

然后修改本地开发配置,在 Testcontainers Bean 上添加 @RestartScope 注解:

@Bean
@RestartScope
@ServiceConnection
public MongoDBContainer mongoDBContainer() {
    return new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"));
}

现在从 test 包的 main() 方法启动应用,即可利用 spring-boot-devtools 的热重载功能。例如用 Postman 保存数据后重新编译应用:

postman save again

修改请求路径(如从 "characters" 改为 "api/characters")并重新编译:

devtools ive reload

从应用日志或 Docker 可见数据库容器未重启。进一步验证应用重启后仍连接同一数据库:向新路径发送 GET 请求,确认之前插入的数据存在:

app reconnected

也可使用 Testcontainers API 的 withReuse(true) 方法:

@Bean
@ServiceConnection
public MongoDBContainer mongoDBContainer() {
    return new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"))
      .withReuse(true);
}

这是更强大的方案,使容器生命周期独立于应用。启用复用后,应用重载或完全重启时容器仍保持运行。

6. 总结

本文介绍了 SpringBoot 3.1 的 Testcontainers 新特性:

  • ✅ 使用 @ServiceConnection 替代 @DynamicPropertySource,简化配置
  • ✅ 通过在 test 包创建启动类,将 Testcontainers 用于本地开发
  • ✅ 结合 spring-boot-devtools@RestartScope 实现容器复用,提升开发效率

完整代码示例见 GitHub 仓库


原始标题:Built-in Testcontainers Support in Spring Boot