13.2 Testcontainers

Testcontainers 响应式集成测试:PostgreSQL 和 Redis 容器生命周期与动态端口映射、R2DBC JDBC 双连接动态配置、Flyway Schema 初始化与测试数据准备、容器复用与并行测试策略、CI 环境 Docker 配置。

Testcontainers 简介

Testcontainers 在测试时自动启动 Docker 容器(PostgreSQL、Redis、Kafka 等),测试完成后自动销毁。保证:

  • 每个测试有独立干净的数据库
  • 测试环境一致(不依赖本地已安装的数据库)
  • CI/CD 流水线可运行

依赖

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>

PostgreSQL 容器测试

import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.util.concurrent.atomic.AtomicReference;

@Testcontainers
@SpringBootTest
public class OrderRepositoryTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17")
        .withDatabaseName("eventd_test")
        .withUsername("eventd")
        .withPassword("eventd");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.r2dbc.url", () ->
            "r2dbc:postgresql://" + postgres.getHost() + ":" + postgres.getFirstMappedPort() + "/eventd_test");
        registry.add("spring.r2dbc.username", postgres::getUsername);
        registry.add("spring.r2dbc.password", postgres::getPassword);
        registry.add("spring.flyway.url", postgres::getJdbcUrl);
        registry.add("spring.flyway.user", postgres::getUsername);
        registry.add("spring.flyway.password", postgres::getPassword);
    }

    @Autowired
    private OrderRepository orderRepo;

    @Test
    void testSaveAndFind() {
        Order order = new Order(null, "BTC", 1, 40000.0);
        AtomicReference<Long> savedId = new AtomicReference<>();

        StepVerifier.create(orderRepo.save(order))
            .assertNext(saved -> {
                assertNotNull(saved.id());
                savedId.set(saved.id());
            })
            .expectComplete()
            .verify();

        StepVerifier.create(orderRepo.findById(savedId.get()))
            .assertNext(found -> assertEquals("BTC", found.symbol()))
            .expectComplete()
            .verify();
    }
}

@DynamicPropertySource 动态覆盖 Spring 配置,指向 Testcontainers 启动的随机端口。

Redis 容器测试

@Testcontainers
@SpringBootTest
public class CacheServiceTest {

    @Container
    static GenericContainer<?> redis = new GenericContainer<>("redis:7")
        .withExposedPorts(6379);

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        // Spring Boot 3 的 Redis 属性在 spring.data.redis 下
        registry.add("spring.data.redis.host", redis::getHost);
        registry.add("spring.data.redis.port", redis::getFirstMappedPort);
    }

    @Autowired
    private RedisCacheService cacheService;

    @Test
    void testCacheSetAndGet() {
        StepVerifier.create(cacheService.set("key1", "value1", Duration.ofMinutes(1)))
            .expectNext("OK")
            .expectComplete()
            .verify();

        StepVerifier.create(cacheService.get("key1"))
            .expectNext("value1")
            .expectComplete()
            .verify();
    }
}

多容器组合

同时启动 PostgreSQL + Redis:

@Testcontainers
public class IntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17")
        .withDatabaseName("eventd_test")
        .withUsername("eventd")
        .withPassword("eventd");

    @Container
    static GenericContainer<?> redis = new GenericContainer<>("redis:7")
        .withExposedPorts(6379);

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.r2dbc.url", () ->
            "r2dbc:postgresql://" + postgres.getHost() + ":" + postgres.getFirstMappedPort() + "/eventd_test");
        registry.add("spring.r2dbc.username", postgres::getUsername);
        registry.add("spring.r2dbc.password", postgres::getPassword);
        registry.add("spring.data.redis.host", redis::getHost);
        registry.add("spring.data.redis.port", redis::getFirstMappedPort);
    }
}

容器复用

每个测试类都启动新容器太慢。Testcontainers 提供的是容器复用,不是重启策略:

@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17")
    .withDatabaseName("eventd_test")
    .withReuse(true);  // 复用容器

withReuse(true) 在 JVM 退出时保留容器,下一次使用完全相同配置时复用。需要开启 Testcontainers 的复用支持:

# ~/.testcontainers.properties
testcontainers.reuse.enable=true

数据清理

每个测试后清理数据,避免相互影响:

@BeforeEach
void setUp() {
    // 清理 orders 表
    StepVerifier.create(db.sql("TRUNCATE orders, users, inventory CASCADE").then())
        .expectComplete()
        .verify();
}

也可以用 Spring Test 的 @Transactional 自动回滚,但响应式测试需要正确配置 ReactiveTransactionManager,且测试代码里的 subscribe() 是否在同一事务上下文里要特别小心。初学和排查阶段,显式清理更直观。

注意:复用容器 + @BeforeEach 清库时,测试类之间不要并行跑同一个库,否则会互相删数据。需要并行就每个测试用独立 schema,或每个测试类起独立容器。

CI 配置

GitHub Actions 示例:

name: CI
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '25'
          distribution: 'temurin'
      - name: Run tests
        run: ./mvnw test

Testcontainers 自动检测 CI 环境的 Docker(GitHub Actions 已预装)。

小结

能力API
启动数据库new PostgreSQLContainer<>("postgres:17")
动态配置@DynamicPropertySource
多容器多个 @Container 字段
复用.withReuse(true)
清理@BeforeEach + TRUNCATE
CI 支持开箱即用

下一节 13.3 性能测试 :并发压测、JMH 微基准、延迟分布分析。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南