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 微基准、延迟分布分析。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。