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 では、次のサービス接続ファクトリが提供されます。
接続詳細 | 一致 |
---|---|
"symptoma/activemq" または | |
| |
| |
| |
| |
| |
| |
| |
"osixia/openldap" という名前のコンテナーまたは型 | |
| |
| |
| |
"otel/opentelemetry-collector-contrib" という名前のコンテナーまたは型 | |
"otel/opentelemetry-collector-contrib" という名前のコンテナーまたは型 | |
"otel/opentelemetry-collector-contrib" という名前のコンテナーまたは型 | |
| |
型 | |
| |
型 | |
"openzipkin/zipkin" という名前のコンテナー |
デフォルトでは、特定の 適用可能な型のサブセットのみを作成する場合は、 |
デフォルトでは、接続の詳細を見つけるために使用される名前を取得するために 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 と通信できるようになります。