Testcontainers

Testcontainers (英語) ライブラリは、Docker コンテナー内で実行されるサービスを管理する方法を提供します。JUnit と統合されているため、テストを実行する前にコンテナーを起動できるテストクラスを作成できます。Testcontainers は、MySQL、MongoDB、Cassandra などの実際のバックエンドサービスと通信する統合テストを作成する場合に特に役立ちます。

次のセクションでは、テストコンテナーをテストに統合するために使用できるいくつかの方法について説明します。

Spring Bean の使用

Testcontainers によって提供されるコンテナーは、Spring Boot によって Bean として管理できます。

コンテナーを Bean として宣言するには、テスト構成に @Bean (Javadoc) メソッドを追加します。

  • Java

  • Kotlin

import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.utility.DockerImageName;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;

@TestConfiguration(proxyBeanMethods = false)
class MyTestConfiguration {

	@Bean
	MongoDBContainer mongoDbContainer() {
		return new MongoDBContainer(DockerImageName.parse("mongo:5.0"));
	}

}
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.testcontainers.containers.MongoDBContainer
import org.testcontainers.utility.DockerImageName

@TestConfiguration(proxyBeanMethods = false)
class MyTestConfiguration {

	@Bean
	fun mongoDbContainer(): MongoDBContainer {
		return MongoDBContainer(DockerImageName.parse("mongo:5.0"))
	}

}

次に、テストクラスに構成クラスをインポートして、コンテナーを挿入して使用できます。

  • Java

  • Kotlin

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.MongoDBContainer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

@SpringBootTest
@Import(MyTestConfiguration.class)
class MyIntegrationTests {

	@Autowired
	private MongoDBContainer mongo;

	@Test
	void myTest() {
		...
	}

}
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Import
import org.testcontainers.containers.MongoDBContainer

@SpringBootTest
@Import(MyTestConfiguration::class)
class MyIntegrationTests {

	@Autowired
	private val mongo: MongoDBContainer? = null

	@Test
	fun myTest() {
		...
	}

}
このコンテナー管理方法は、サービス接続アノテーションと組み合わせて使用されることが多いです。

JUnit 拡張機能の使用

Testcontainers は、テスト内のコンテナーを管理するための JUnit 拡張機能を提供します。この拡張機能は、Testcontainers の @Testcontainers (英語) アノテーションをテストクラスに適用することで有効化されます。

その後、静的コンテナーフィールドで @Container (英語) アノテーションを使用できます。

@Testcontainers (英語) アノテーションは、標準の JUnit テストで使用することも、@SpringBootTest (Javadoc) と組み合わせて使用することもできます。

  • Java

  • Kotlin

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import org.springframework.boot.test.context.SpringBootTest;

@Testcontainers
@SpringBootTest
class MyIntegrationTests {

	@Container
	static Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:5");

	@Test
	void myTest() {
		...
	}

}
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import org.springframework.boot.test.context.SpringBootTest;

@Testcontainers
@SpringBootTest
class MyIntegrationTests {

	@Test
	fun myTest() {
		...
	}

	companion object {

		@Container
		@JvmStatic
		val neo4j = Neo4jContainer("neo4j:5");

	}
}

上記の例では、テストを実行する前に Neo4j コンテナーを起動します。コンテナーインスタンスのライフサイクルは、公式ドキュメント (英語) に記載されているように、Testcontainers によって管理されます。

ほとんどの場合、コンテナー内で実行されているサービスに接続するようにアプリケーションを追加で構成する必要があります。

コンテナー構成インターフェースのインポート

Testcontainers の一般的なパターンは、コンテナーインスタンスをインターフェース内の静的フィールドとして宣言することです。

例: 次のインターフェースは、型 MongoDBContainer (英語) の mongo という名前のコンテナーと型 Neo4jContainer (英語) の neo4j という名前のコンテナーの 2 つのコンテナーを宣言します。

  • Java

  • Kotlin

import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;

interface MyContainers {

	@Container
	MongoDBContainer mongoContainer = new MongoDBContainer("mongo:5.0");

	@Container
	Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:5");

}
import org.testcontainers.containers.MongoDBContainer
import org.testcontainers.containers.Neo4jContainer
import org.testcontainers.junit.jupiter.Container

interface MyContainers {

	companion object {

		@Container
		val mongoContainer: MongoDBContainer = MongoDBContainer("mongo:5.0")

		@Container
		val neo4jContainer: Neo4jContainer<*> = Neo4jContainer("neo4j:5")

	}

}

このようにコンテナーを宣言すると、テストクラスにインターフェースを実装させることで、複数のテストでその構成を再利用できます。

Spring Boot テストでも同じインターフェース設定を使用できます。そのためには、テスト設定クラスに @ImportTestcontainers (Javadoc) を追加します。

  • Java

  • Kotlin

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.context.ImportTestcontainers;

@TestConfiguration(proxyBeanMethods = false)
@ImportTestcontainers(MyContainers.class)
class MyTestConfiguration {

}
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.testcontainers.context.ImportTestcontainers

@TestConfiguration(proxyBeanMethods = false)
@ImportTestcontainers(MyContainers::class)
class MyTestConfiguration {

}

管理コンテナーのライフサイクル

Testcontainers が提供するアノテーションと拡張機能を使用している場合、コンテナーインスタンスのライフサイクルは Testcontainers によって完全に管理されます。詳細については、Testcontainers の公式ドキュメント (英語) を参照してください。

コンテナーが Spring によって Bean として管理される場合、そのライフサイクルは Spring によって管理されます。

  • コンテナー Bean は、他のすべての Bean よりも前に作成され、開始されます。

  • コンテナー Bean は、他のすべての Bean が破棄された後に停止されます。

このプロセスにより、コンテナーが提供する機能に依存するすべての Bean がそれらの機能を利用できるようになります。また、コンテナーが利用可能な間にそれらの Bean がクリーンアップされることも保証されます。

アプリケーション Bean がコンテナーの機能に依存している場合は、正しいライフサイクル動作を確保するために、コンテナーを Spring Bean として構成することをお勧めします。
コンテナーを Spring Bean ではなく Testcontainers で管理する場合、Bean とコンテナーのシャットダウン順序は保証されません。コンテナー機能に依存する Bean がクリーンアップされる前にコンテナーがシャットダウンされる可能性があります。その結果、たとえば接続の喪失などにより、クライアント Bean から例外がスローされる可能性があります。

コンテナー Bean は、Spring の TestContext フレームワークによって管理されるアプリケーションコンテキストごとに 1 回作成および起動されます。TestContext フレームワークが基盤となるアプリケーションコンテキストとその中の Bean を管理する方法の詳細については、Spring Framework ドキュメントを参照してください。

コンテナー Bean は、TestContext フレームワークの標準的なアプリケーションコンテキストシャットダウンプロセスの一環として停止されます。アプリケーションコンテキストがシャットダウンされると、コンテナーもシャットダウンされます。これは通常、特定のキャッシュされたアプリケーションコンテキストを使用するすべてのテストの実行が終了した後に発生します。TestContext フレームワークで設定されたキャッシュ動作によっては、それよりも早く発生する場合もあります。

単一のテストコンテナーインスタンスは、複数のテストクラスからのテストの実行にわたって保持することができ、多くの場合、保持されます。

サービス接続

サービス接続は、任意の リモートサービスへの接続です。Spring Boot の自動構成は、サービス接続の詳細を消費し、使用して リモートサービスへの接続を確立できます。その場合、接続の詳細は、接続関連の構成プロパティよりも優先されます。

Testcontainers を使用すると、テストクラスのコンテナーフィールドにアノテーションを付けることで、コンテナーで実行されているサービスの接続の詳細を自動的に作成できます。

  • Java

  • Kotlin

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;

@Testcontainers
@SpringBootTest
class MyIntegrationTests {

	@Container
	@ServiceConnection
	static Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:5");

	@Test
	void myTest() {
		...
	}

}
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;

@Testcontainers
@SpringBootTest
class MyIntegrationTests {

	@Test
	fun myTest() {
		...
	}

	companion object {

		@Container
		@ServiceConnection
		@JvmStatic
		val neo4j = Neo4jContainer("neo4j:5");

	}

}

@ServiceConnection (Javadoc) のおかげで、上記の構成により、アプリケーション内の Neo4j 関連の Bean は、Testcontainers が管理する Docker コンテナー内で実行されている Neo4j と通信できるようになります。これは、Neo4jConnectionDetails (Javadoc) Bean を自動的に定義することによって行われ、これは Neo4j の自動構成によって使用され、接続関連の構成プロパティが上書きされます。

Testcontainers とのサービス接続を使用するには、spring-boot-testcontainers モジュールをテストの依存関係として追加する必要があります。

サービス接続アノテーションは、spring.factories に登録された ContainerConnectionDetailsFactory (Javadoc) クラスによって処理されます。ContainerConnectionDetailsFactory (Javadoc) は、特定の Container (英語) サブクラスまたは Docker イメージ名に基づいて、ConnectionDetails (Javadoc) Bean を作成できます。

spring-boot-testcontainers jar では、次のサービス接続ファクトリが提供されます。

接続詳細 一致

ActiveMQConnectionDetails (Javadoc)

"symptoma/activemq" または ActiveMQContainer (英語) という名前のコンテナー

ArtemisConnectionDetails (Javadoc)

ArtemisContainer (英語) 型のコンテナー

CassandraConnectionDetails (Javadoc)

CassandraContainer (英語) 型のコンテナー

CouchbaseConnectionDetails (Javadoc)

CouchbaseContainer (英語) 型のコンテナー

ElasticsearchConnectionDetails (Javadoc)

ElasticsearchContainer (英語) 型のコンテナー

FlywayConnectionDetails (Javadoc)

JdbcDatabaseContainer (英語) 型のコンテナー

JdbcConnectionDetails (Javadoc)

JdbcDatabaseContainer (英語) 型のコンテナー

KafkaConnectionDetails (Javadoc)

KafkaContainer (英語) ConfluentKafkaContainer (英語) RedpandaContainer (英語) 型のコンテナー

LdapConnectionDetails (Javadoc)

"osixia/openldap" という名前のコンテナーまたは型 LLdapContainer (英語) のコンテナー

LiquibaseConnectionDetails (Javadoc)

JdbcDatabaseContainer (英語) 型のコンテナー

MongoConnectionDetails (Javadoc)

MongoDBContainer (英語) 型のコンテナー

Neo4jConnectionDetails (Javadoc)

Neo4jContainer (英語) 型のコンテナー

OtlpLoggingConnectionDetails (Javadoc)

"otel/opentelemetry-collector-contrib" という名前のコンテナーまたは型 LgtmStackContainer のコンテナー

OtlpMetricsConnectionDetails (Javadoc)

"otel/opentelemetry-collector-contrib" という名前のコンテナーまたは型 LgtmStackContainer のコンテナー

OtlpTracingConnectionDetails (Javadoc)

"otel/opentelemetry-collector-contrib" という名前のコンテナーまたは型 LgtmStackContainer のコンテナー

PulsarConnectionDetails (Javadoc)

PulsarContainer (英語) 型のコンテナー

R2dbcConnectionDetails (Javadoc)

型 ClickHouseContainerMariaDBContainer (英語) MSSQLServerContainer (英語) MySQLContainer (英語) OracleContainer (フリー) (英語) OracleContainer (XE) (英語) PostgreSQLContainer (英語) のコンテナー

RabbitConnectionDetails (Javadoc)

RabbitMQContainer (英語) 型のコンテナー

RedisConnectionDetails (Javadoc)

RedisContainer (英語) または RedisStackContainer (英語) のコンテナー、または "redis"、"redis/redis-stack"、"redis/redis-stack-server" という名前のコンテナー

ZipkinConnectionDetails (Javadoc)

"openzipkin/zipkin" という名前のコンテナー

デフォルトでは、特定の Container (英語) に対して適用可能なすべての接続詳細 Bean が作成されます。例: PostgreSQLContainer (英語) JdbcConnectionDetails (Javadoc) R2dbcConnectionDetails (Javadoc) の両方を作成します。

適用可能な型のサブセットのみを作成する場合は、@ServiceConnection (Javadoc) の type 属性を使用できます。

デフォルトでは、接続の詳細を見つけるために使用される名前を取得するために Container.getDockerImageName().getRepository() が使用されます。Docker イメージ名のリポジトリ部分は、レジストリとバージョンを無視します。これは、Spring Boot が Container (英語) のインスタンスを取得できる限り機能します。これは、上記の例のように static フィールドを使用する場合に当てはまります。

@Bean (Javadoc) メソッドを使用している場合、Spring Boot は Bean メソッドを呼び出して Docker イメージ名を取得しません。これは、早期初期化の問題が発生するためです。代わりに、Bean メソッドの戻り値の型を使用して、どの接続詳細を使用する必要があるかを判断します。これは、Neo4jContainer (英語) RabbitMQContainer (英語) などの型指定されたコンテナーを使用している限り機能します。GenericContainer (英語) を使用している場合、たとえば次の例に示すように Redis を使用している場合は機能しなくなります。

  • Java

  • Kotlin

import org.testcontainers.containers.GenericContainer;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;

@TestConfiguration(proxyBeanMethods = false)
public class MyRedisConfiguration {

	@Bean
	@ServiceConnection(name = "redis")
	public GenericContainer<?> redisContainer() {
		return new GenericContainer<>("redis:7");
	}

}
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.testcontainers.service.connection.ServiceConnection
import org.springframework.context.annotation.Bean
import org.testcontainers.containers.GenericContainer

@TestConfiguration(proxyBeanMethods = false)
class MyRedisConfiguration {

	@Bean
	@ServiceConnection(name = "redis")
	fun redisContainer(): GenericContainer<*> {
		return GenericContainer("redis:7")
	}

}

Spring Boot は、GenericContainer (英語) からどのコンテナーイメージが使用されているかを判断できないため、そのヒントを提供するには、@ServiceConnection (Javadoc) の name 属性を使用する必要があります。

また、カスタムイメージを使用する場合など、@ServiceConnection (Javadoc) の name 属性を使用して、使用する接続の詳細をオーバーライドすることもできます。Docker イメージ registry.mycompany.com/mirror/myredis を使用している場合は、RedisConnectionDetails (Javadoc) が確実に作成されるように @ServiceConnection(name="redis") を使用します。

サービス接続での SSL

サポートされているコンテナーで @Ssl (Javadoc) @JksKeyStore (Javadoc) @JksTrustStore (Javadoc) @PemKeyStore (Javadoc) @PemTrustStore (Javadoc) アノテーションを使用して、そのサービス接続の SSL サポートを有効にすることができます。テストコンテナー内で実行されているサービスで SSL を有効にする必要があることに注意してください。アノテーションは、アプリケーションのクライアント側でのみ SSL を構成します。

import com.redis.testcontainers.RedisContainer;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.PemKeyStore;
import org.springframework.boot.testcontainers.service.connection.PemTrustStore;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.data.redis.core.RedisOperations;

@Testcontainers
@SpringBootTest
class MyRedisWithSslIntegrationTests {

	@Container
	@ServiceConnection
	@PemKeyStore(certificate = "classpath:client.crt", privateKey = "classpath:client.key")
	@PemTrustStore("classpath:ca.crt")
	static RedisContainer redis = new SecureRedisContainer("redis:latest");

	@Autowired
	private RedisOperations<Object, Object> operations;

	@Test
	void testRedis() {
		// ...
	}

}

上記のコードでは、@PemKeyStore (Javadoc) アノテーションを使用してクライアント証明書とキーをキーストアに読み込み、@PemTrustStore (Javadoc) アノテーションを使用して CA 証明書をトラストストアに読み込みます。これにより、クライアントがサーバーに対して認証され、トラストストアの CA 証明書によって、サーバー証明書が有効で信頼できることが確認されます。

この例の SecureRedisContainer は、証明書を適切な場所にコピーし、SSL を有効にするコマンドラインパラメーターを使用して redis-server を呼び出す RedisContainer のカスタムサブクラスです。

SSL アノテーションは、次のサービス接続でサポートされています。

  • Cassandra

  • Couchbase

  • Elasticsearch

  • Kafka

  • MongoDB

  • RabbitMQ

  • Redis

ElasticsearchContainer は、サーバー側 SSL の自動検出もサポートしています。この機能を使用するには、次の例に示すように、コンテナーに @Ssl (Javadoc) のアノテーションを付けます。そうすると、Spring Boot がクライアント側の SSL 構成を処理します。

import org.junit.jupiter.api.Test;
import org.testcontainers.elasticsearch.ElasticsearchContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.elasticsearch.DataElasticsearchTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testcontainers.service.connection.Ssl;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate;

@Testcontainers
@DataElasticsearchTest
class MyElasticsearchWithSslIntegrationTests {

	@Ssl
	@Container
	@ServiceConnection
	static ElasticsearchContainer elasticsearch = new ElasticsearchContainer(
			"docker.elastic.co/elasticsearch/elasticsearch:8.17.2");

	@Autowired
	private ElasticsearchTemplate elasticsearchTemplate;

	@Test
	void testElasticsearch() {
		// ...
	}

}

動的プロパティ

サービス接続のやや冗長ですが、より柔軟な代替手段は @DynamicPropertySource (Javadoc) です。静的 @DynamicPropertySource (Javadoc) メソッドを使用すると、動的なプロパティ値を Spring 環境に追加できます。

  • Java

  • Kotlin

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;

@Testcontainers
@SpringBootTest
class MyIntegrationTests {

	@Container
	static Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:5");

	@Test
	void myTest() {
		// ...
	}

	@DynamicPropertySource
	static void neo4jProperties(DynamicPropertyRegistry registry) {
		registry.add("spring.neo4j.uri", neo4j::getBoltUrl);
	}

}
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.testcontainers.containers.Neo4jContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers

@Testcontainers
@SpringBootTest
class MyIntegrationTests {

	@Test
	fun myTest() {
		...
	}

	companion object {
		@Container
		@JvmStatic
		val neo4j = Neo4jContainer("neo4j:5");

		@DynamicPropertySource
		@JvmStatic
		fun neo4jProperties(registry: DynamicPropertyRegistry) {
			registry.add("spring.neo4j.uri") { neo4j.boltUrl }
		}
	}
}

上記の構成により、アプリケーション内の Neo4j 関連の Bean が、Testcontainers が管理する Docker コンテナー内で実行されている Neo4j と通信できるようになります。