关于 testcontainers
testcontainers 是一份使用容器中的真实服务编写 Java 集成测试的参考技能,面向 Java 开发者。内容包括数据库和消息系统示例、容器生命周期管理、Spring Boot 连接方式及故障排查。
主要功能
- 01数据库与消息系统容器示例
- 02Maven 和 Gradle 的 JUnit Jupiter 配置
- 03Spring Boot @ServiceConnection 用法
- 04容器生命周期与复用建议
- 05网络、Compose 和等待策略
适用场景
- 01测试 Java 应用与 PostgreSQL 或 MongoDB 的交互
- 02验证 Kafka 或 RabbitMQ 消息流程
- 03使用容器运行 Spring Boot 集成测试
- 04排查启动缓慢和端口冲突
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill testcontainers安装后在 Claude Code 中输入/testcontainers确认已加载。
详细介绍
Testcontainers - 快速参考
深入了解:使用
mcp__documentation__fetch_docs,并指定技术:testcontainers,获取全面的文档。
不应使用此技能的情况
- 单元测试 - 使用
junit和 Mockito 进行快速、隔离的测试 - 仅测试 REST API - 如果 API 使用模拟对象,则不使用容器,改用
rest-assured - 没有 Docker 的环境 - 使用 H2 或嵌入式数据库
- 资源受限的 CI - 容器可能过于占用资源,请改用模拟对象
- 非 Java 项目 - 查看语言专用的 Testcontainers 库
基础设置
Maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mongodb</artifactId>
<scope>test</scope>
</dependency>
Gradle 依赖
testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:postgresql")
testImplementation("org.testcontainers:mongodb")
@ServiceConnection(Spring Boot 3.1+)
推荐模式
@SpringBootTest
@Testcontainers
class MyIntegrationTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine");
@Test
void testWithDatabase() {
// Connection auto-configured
}
}
支持的容器
| 容器 | Maven 构件 | 连接详情 |
|---|---|---|
| PostgreSQLContainer | postgresql | JDBC + R2DBC |
| MySQLContainer | mysql | JDBC + R2DBC |
| MariaDBContainer | mariadb | JDBC + R2DBC |
| MongoDBContainer | mongodb | MongoConnectionDetails |
| KafkaContainer | kafka | KafkaConnectionDetails |
| RedisContainer | - | RedisConnectionDetails |
| RabbitMQContainer | rabbitmq | RabbitConnectionDetails |
| ElasticsearchContainer | elasticsearch | ElasticsearchConnectionDetails |
| CassandraContainer | cassandra | CassandraConnectionDetails |
配合 @ServiceConnection 使用 GenericContainer
@Container
@ServiceConnection(name = "redis")
static GenericContainer<?> redis =
new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
生命周期管理
静态容器(在测试间共享 - 推荐)
@Testcontainers
class SharedContainerTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine");
@Test
void test1() { /* same container */ }
@Test
void test2() { /* same container */ }
}
Spring Bean 容器(生命周期控制最佳)
@TestConfiguration(proxyBeanMethods = false)
class TestContainersConfig {
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>("postgres:16-alpine");
}
}
@SpringBootTest
@Import(TestContainersConfig.class)
class ManagedContainerTest {
// Container lifecycle managed by Spring
// Started before beans, stopped after beans
}
容器复用(开发环境)
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine")
.withReuse(true);
需要在 ~/.testcontainers.properties 中配置:
testcontainers.reuse.enable=true
数据库容器
PostgreSQL
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test")
.withInitScript("init.sql");
MongoDB
@Container
@ServiceConnection
static MongoDBContainer mongo =
new MongoDBContainer("mongo:7.0")
.withSharding();
MySQL
@Container
@ServiceConnection
static MySQLContainer<?> mysql =
new MySQLContainer<>("mysql:8.0")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
消息传递容器
Kafka
@Container
@ServiceConnection
static KafkaContainer kafka =
new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.0"))
.withKraft();
RabbitMQ
@Container
@ServiceConnection
static RabbitMQContainer rabbitmq =
new RabbitMQContainer("rabbitmq:3.12-management")
.withExposedPorts(5672, 15672);
Redis
@Container
@ServiceConnection
static GenericContainer<?> redis =
new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
消息传递容器测试模式
专用技能:如需全面覆盖消息传递测试,请参阅
messaging-testing-kafka、messaging-testing-rabbitmq和messaging-testing。
Kafka:生产 → 消费 → 断言
@SpringBootTest
@Testcontainers
class KafkaProduceConsumeTest {
@Container
@ServiceConnection
static KafkaContainer kafka = new KafkaContainer(
DockerImageName.parse("apache/kafka-native:3.8.0"));
@Autowired
private KafkaTemplate<String, OrderEvent> kafkaTemplate;
@Autowired
private OrderRepository orderRepository;
@Test
void shouldProcessOrderViaKafka() throws Exception {
kafkaTemplate.send("orders", "key-1",
new OrderEvent("123", "CREATED")).get(10, TimeUnit.SECONDS);
await().atMost(Duration.ofSeconds(10))
.untilAsserted(() ->
assertThat(orderRepository.findById("123")).isPresent());
}
}
RabbitMQ:发送 → 监听 → 断言
@SpringBootTest
@Testcontainers
class RabbitProduceConsumeTest {
@Container
@ServiceConnection
static RabbitMQContainer rabbit = new RabbitMQContainer("rabbitmq:3.13-management");
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private OrderRepository orderRepository;
@Test
void shouldProcessOrderViaRabbit() {
rabbitTemplate.convertAndSend("orders.exchange", "orders.created",
new OrderEvent("456", "CREATED"));
await().atMost(Duration.ofSeconds(10))
.untilAsserted(() ->
assertThat(orderRepository.findById("456")).isPresent());
}
}
Redis Pub/Sub:发布 → 订阅 → 断言
@SpringBootTest
@Testcontainers
class RedisPubSubTest {
@Container
@ServiceConnection(name = "redis")
static GenericContainer<?> redis =
new GenericContainer<>("redis:7-alpine").withExposedPorts(6379);
@Autowired
private StringRedisTemplate redisTemplate;
@Test
void shouldPublishAndReceiveMessage() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
List<String> received = new CopyOnWriteArrayList<>();
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisTemplate.getConnectionFactory());
container.addMessageListener((message, pattern) -> {
received.add(new String(message.getBody()));
latch.countDown();
}, new ChannelTopic("orders"));
container.afterPropertiesSet();
container.start();
redisTemplate.convertAndSend("orders", "{\"orderId\":\"789\"}");
assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
assertThat(received.get(0)).contains("789");
container.stop();
}
}
旧版模式(@DynamicPropertySource)
@Testcontainers
@SpringBootTest
class LegacyTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16");
@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);
}
}
网络与 Compose
容器网络
@Testcontainers
class NetworkTest {
static Network network = Network.newNetwork();
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16")
.withNetwork(network)
.withNetworkAliases("postgres");
@Container
static GenericContainer<?> app =
new GenericContainer<>("myapp:latest")
.withNetwork(network)
.dependsOn(postgres)
.withEnv("DATABASE_HOST", "postgres");
}
Docker Compose
@Testcontainers
class ComposeTest {
@Container
static DockerComposeContainer<?> compose =
new DockerComposeContainer<>(new File("docker-compose-test.yml"))
.withExposedService("postgres", 5432)
.withExposedService("redis", 6379);
@Test
void test() {
String host = compose.getServiceHost("postgres", 5432);
int port = compose.getServicePort("postgres", 5432);
}
}
等待策略
new GenericContainer<>("custom-image")
.waitingFor(Wait.forHttp("/health").forStatusCode(200))
.waitingFor(Wait.forLogMessage(".*Started.*", 1))
.waitingFor(Wait.forListeningPort())
.withStartupTimeout(Duration.ofMinutes(2));
最佳实践
| 应做 | 不应做 |
|---|---|
使用 static 容器 | 每个测试方法都创建一个容器 |
使用 @ServiceConnection | 手动配置属性 |
| 使用 Spring Bean 生命周期 | 对依赖应用的容器使用 JUnit 生命周期 |
| 在开发环境中启用容器复用 | 每次运行都启动全新容器 |
| 使用特定的镜像标签 | 使用 latest 标签 |
| 通过基类共享容器 | 重复声明容器 |
常见问题
容器无法启动
// Check Docker is running
// Check image exists
// Increase startup timeout
.withStartupTimeout(Duration.ofMinutes(5))
端口冲突
// Always use exposed port mapping
container.getMappedPort(5432)
// Never hardcode ports
测试速度慢
// Enable reuse
.withReuse(true)
// Use lighter images (-alpine)
new PostgreSQLContainer<>("postgres:16-alpine")
反模式
| 反模式 | 不良影响 | 解决方案 |
|---|---|---|
| 每个测试方法都创建一个容器 | 速度极慢 | 使用在测试间共享的静态容器 |
| 不使用 @ServiceConnection | 手动重复配置 | 让 Spring 根据容器自动配置 |
| 使用 latest 标签 | 测试结果不确定 | 固定特定版本(postgres:16-alpine) |
| 本地开发时未启用 withReuse | 开发反馈周期缓慢 | 在 ~/.testcontainers.properties 中启用复用 |
| 硬编码端口 | 端口冲突 | 使用 getMappedPort() 获取动态端口 |
| 忽略启动超时 | 测试挂起 | 适当设置 withStartupTimeout |
| 不清理测试数据 | 测试之间相互干扰 | 使用 @Transactional 或手动清理 |
快速故障排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| "找不到镜像" | Docker 未运行或镜像不可用 | 启动 Docker,检查镜像名称 |
| 容器启动超时 | 镜像过大或启动缓慢 | 增加超时时间,使用更轻量的镜像 |
| 端口已被占用 | 上一次测试未清理 | 使用 getMappedPort() 分配动态端口 |
| 测试非常慢 | 每次测试都启动容器 | 使用静态容器 |
| 连接被拒绝 | 使用了 localhost 而非容器主机 | 使用 container.getHost() 和 getMappedPort() |
| "@ServiceConnection 不起作用" | Spring Boot 版本不正确 | 需要 Spring Boot 3.1+,请检查版本 |
参考文档
评论
支持 Markdown