mcpField
T
testcontainers

testcontainers

github.com/claude-dev-suite测试与质量4659 次安装4 天前更新
代码仓库

关于 testcontainers

testcontainers 是一份使用容器中的真实服务编写 Java 集成测试的参考技能,面向 Java 开发者。内容包括数据库和消息系统示例、容器生命周期管理、Spring Boot 连接方式及故障排查。

主要功能

  1. 01数据库与消息系统容器示例
  2. 02Maven 和 Gradle 的 JUnit Jupiter 配置
  3. 03Spring Boot @ServiceConnection 用法
  4. 04容器生命周期与复用建议
  5. 05网络、Compose 和等待策略

适用场景

  1. 01测试 Java 应用与 PostgreSQL 或 MongoDB 的交互
  2. 02验证 Kafka 或 RabbitMQ 消息流程
  3. 03使用容器运行 Spring Boot 集成测试
  4. 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 构件连接详情
PostgreSQLContainerpostgresqlJDBC + R2DBC
MySQLContainermysqlJDBC + R2DBC
MariaDBContainermariadbJDBC + R2DBC
MongoDBContainermongodbMongoConnectionDetails
KafkaContainerkafkaKafkaConnectionDetails
RedisContainer-RedisConnectionDetails
RabbitMQContainerrabbitmqRabbitConnectionDetails
ElasticsearchContainerelasticsearchElasticsearchConnectionDetails
CassandraContainercassandraCassandraConnectionDetails
配合 @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