testcontainers について
testcontainersは、コンテナで実サービスを動かし、Javaの統合テストを書くためのリファレンススキルです。データベースやメッセージングの例、ライフサイクル管理、Spring Bootとの接続、トラブルシューティングを扱います。
主な機能
- 01データベースとメッセージングのコンテナ例
- 02MavenとGradleでのJUnit Jupiter設定
- 03Spring Bootの@ServiceConnectionパターン
- 04コンテナのライフサイクルと再利用の指針
- 05ネットワーク、Compose、待機戦略
活用シーン
- 01JavaアプリとPostgreSQLやMongoDBの連携テスト
- 02KafkaやRabbitMQのメッセージ処理フローの検証
- 03コンテナを使ったSpring Boot統合テスト
- 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 アーティファクト | 接続の詳細 |
|---|---|---|
| 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 または手動クリーンアップを使用する |
トラブルシューティング(クイック)
| 問題 | 考えられる原因 | 解決策 |
|---|---|---|
| "Could not find image" | Docker が起動していない、またはイメージを利用できない | Docker を起動し、イメージ名を確認します |
| コンテナーの起動がタイムアウトする | イメージが大きすぎる、または起動が遅い | タイムアウトを延長するか、より軽量なイメージを使用します |
| ポートがすでに使用中 | 前回のテストでクリーンアップされなかった | getMappedPort() を使って動的ポートを使用します |
| テストが非常に遅い | テストごとにコンテナーを起動している | 静的コンテナーを使用します |
| 接続が拒否される | コンテナーホストではなく localhost を使用している | container.getHost() と getMappedPort() を使用します |
| "@ServiceConnection が動作しない" | Spring Boot のバージョンが異なる | Spring Boot 3.1 以降が必要です。バージョンを確認してください |
リファレンスドキュメント
コメント
Markdown 対応