mcpField
T
testcontainers

testcontainers

github.com/claude-dev-suiteテスト・品質4659 回インストール4 日前に更新
リポジトリ

testcontainers について

testcontainersは、コンテナで実サービスを動かし、Javaの統合テストを書くためのリファレンススキルです。データベースやメッセージングの例、ライフサイクル管理、Spring Bootとの接続、トラブルシューティングを扱います。

主な機能

  1. 01データベースとメッセージングのコンテナ例
  2. 02MavenとGradleでのJUnit Jupiter設定
  3. 03Spring Bootの@ServiceConnectionパターン
  4. 04コンテナのライフサイクルと再利用の指針
  5. 05ネットワーク、Compose、待機戦略

活用シーン

  1. 01JavaアプリとPostgreSQLやMongoDBの連携テスト
  2. 02Kafkaや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 - クイックリファレンス

深い知識:包括的なドキュメントについては、テクノロジーとして testcontainers を指定して mcp__documentation__fetch_docs を使用してください。

このスキルを使用しない場合

  • 単体テスト - 高速で分離されたテストには、Mockito とともに junit を使用してください
  • 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 または手動クリーンアップを使用する

トラブルシューティング(クイック)

問題考えられる原因解決策
"Could not find image"Docker が起動していない、またはイメージを利用できないDocker を起動し、イメージ名を確認します
コンテナーの起動がタイムアウトするイメージが大きすぎる、または起動が遅いタイムアウトを延長するか、より軽量なイメージを使用します
ポートがすでに使用中前回のテストでクリーンアップされなかったgetMappedPort() を使って動的ポートを使用します
テストが非常に遅いテストごとにコンテナーを起動している静的コンテナーを使用します
接続が拒否されるコンテナーホストではなく localhost を使用しているcontainer.getHost() と getMappedPort() を使用します
"@ServiceConnection が動作しない"Spring Boot のバージョンが異なるSpring Boot 3.1 以降が必要です。バージョンを確認してください

リファレンスドキュメント

コメント