Testcontainers

Testcontainers 庫提供了一種管理在 Docker 容器中執行服務的方法。它與 JUnit 整合,允許您編寫一個測試類,在任何測試執行之前啟動一個容器。Testcontainers 對於編寫與真實後端服務(如 MySQL、MongoDB、Cassandra 等)通訊的整合測試特別有用。

在以下部分中,我們將介紹一些您可以用來將 Testcontainers 與測試整合的方​​法。

使用 Spring Bean

Testcontainers 提供的容器可以由 Spring Boot 作為 Bean 進行管理。

要將容器宣告為 Bean,請在測試配置中新增一個 @Bean 方法

  • Java

  • Kotlin

import org.testcontainers.mongodb.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.mongodb.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.mongodb.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.mongodb.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 結合使用

  • Java

  • Kotlin

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

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.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.neo4j.Neo4jContainer;

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 的一個常見模式是將容器例項宣告為介面中的靜態欄位。

例如,以下介面聲明瞭兩個容器,一個名為 mongo,型別為 MongoDBContainer,另一個名為 neo4j,型別為 Neo4jContainer

  • Java

  • Kotlin

import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.mongodb.MongoDBContainer;
import org.testcontainers.neo4j.Neo4jContainer;

interface MyContainers {

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

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

}
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.mongodb.MongoDBContainer
import org.testcontainers.neo4j.Neo4jContainer

interface MyContainers {

	companion object {

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

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

	}

}

當您以這種方式宣告容器時,您可以透過讓測試類實現介面來在多個測試中重用它們的配置。

也可以在您的 Spring Boot 測試中使用相同的介面配置。為此,請在測試配置類中新增 @ImportTestcontainers

  • 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 依賴於容器的功能時,優先將容器配置為 Spring Bean,以確保正確的生命週期行為。
讓容器由 Testcontainers 而非 Spring Bean 管理,無法保證 Bean 和容器的關閉順序。可能會出現容器在依賴容器功能的 Bean 清理之前關閉的情況。這可能導致客戶端 Bean 丟擲異常,例如由於連線丟失。

容器 Bean 在由 Spring TestContext 框架管理的每個應用程式上下文建立和啟動一次。有關 TestContext 框架如何管理底層應用程式上下文及其中的 Bean 的詳細資訊,請參閱 Spring 框架文件

容器 Bean 作為 TestContext 框架標準應用程式上下文關閉過程的一部分停止。當應用程式上下文關閉時,容器也會關閉。這通常發生在所有使用該特定快取應用程式上下文的測試執行完畢之後。也可能更早發生,具體取決於 TestContext 框架中配置的快取行為。

單個測試容器例項可以(也通常是)在多個測試類的測試執行中保留。

服務連線

服務連線是與任何遠端服務的連線。Spring Boot 的自動配置可以利用服務連線的詳細資訊,並使用它們來建立與遠端服務的連線。在此過程中,連線詳細資訊優先於任何與連線相關的配置屬性。

使用 Testcontainers 時,可以透過在測試類中註解容器欄位來自動為容器中執行的服務建立連線詳細資訊。

  • Java

  • Kotlin

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

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.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.neo4j.Neo4jContainer;

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,上述配置允許應用程式中與 Neo4j 相關的 Bean 與 Testcontainers 管理的 Docker 容器中執行的 Neo4j 進行通訊。這是透過自動定義一個 Neo4jConnectionDetails Bean 來完成的,該 Bean 隨後由 Neo4j 自動配置使用,覆蓋任何與連線相關的配置屬性。

您需要將 spring-boot-testcontainers 模組新增為測試依賴項,才能將服務連線與 Testcontainers 結合使用。

服務連線註解由註冊到 spring.factoriesContainerConnectionDetailsFactory 類處理。ContainerConnectionDetailsFactory 可以根據特定的 Container 子類或 Docker 映象名稱建立 ConnectionDetails Bean。

spring-boot-testcontainers jar 中提供了以下服務連線工廠

連線詳細資訊 匹配物件

ActiveMQConnectionDetails

名為 "symptoma/activemq" 的容器或 ActiveMQContainer

ArtemisConnectionDetails

型別為 ArtemisContainer 的容器

CassandraConnectionDetails

型別為 CassandraContainer 的容器

CouchbaseConnectionDetails

型別為 CouchbaseContainer 的容器

ElasticsearchConnectionDetails

型別為 ElasticsearchContainer 的容器

FlywayConnectionDetails

型別為 JdbcDatabaseContainer 的容器

JdbcConnectionDetails

型別為 JdbcDatabaseContainer 的容器

KafkaConnectionDetails

型別為 KafkaContainerConfluentKafkaContainerRedpandaContainer 的容器

LdapConnectionDetails

名為 "osixia/openldap" 的容器或型別為 LLdapContainer 的容器

LiquibaseConnectionDetails

型別為 JdbcDatabaseContainer 的容器

MongoConnectionDetails

型別為 MongoDBContainerMongoDBAtlasLocalContainer 的容器

Neo4jConnectionDetails

型別為 Neo4jContainer 的容器

OpenTelemetryLoggingConnectionDetails

名為 "otel/opentelemetry-collector-contrib" 的容器或型別為 LgtmStackContainer 的容器

OtlpMetricsConnectionDetails

名為 "otel/opentelemetry-collector-contrib" 的容器或型別為 LgtmStackContainer 的容器

OtlpTracingConnectionDetails

名為 "otel/opentelemetry-collector-contrib" 的容器或型別為 LgtmStackContainer 的容器

PulsarConnectionDetails

型別為 PulsarContainer 的容器

R2dbcConnectionDetails

型別為 ClickHouseContainerMariaDBContainerMSSQLServerContainerMySQLContainerOracleContainer (free)OracleContainer (XE)PostgreSQLContainer 的容器

RabbitConnectionDetails

型別為 RabbitMQContainer 的容器

RedisConnectionDetails

型別為 RedisContainerRedisStackContainer 的容器,或名為 "redis"、"redis/redis-stack" 或 "redis/redis-stack-server" 的容器

ZipkinConnectionDetails

名為 "openzipkin/zipkin" 的容器

預設情況下,將為給定的 Container 建立所有適用的連線詳細資訊 Bean。例如,一個 PostgreSQLContainer 將同時建立 JdbcConnectionDetailsR2dbcConnectionDetails

如果您只想建立適用型別的一個子集,可以使用 @ServiceConnectiontype 屬性。

預設情況下,使用 Container.getDockerImageName().getRepository() 獲取用於查詢連線詳細資訊的名稱。Docker 映象名稱的儲存庫部分忽略任何登錄檔和版本。只要 Spring Boot 能夠獲取 Container 例項,這種方法就有效,就像上面示例中使用的 static 欄位一樣。

如果您正在使用 @Bean 方法,Spring Boot 不會呼叫 Bean 方法來獲取 Docker 映象名稱,因為這會導致急切初始化問題。相反,Bean 方法的返回型別用於確定應使用哪個連線詳細資訊。只要您使用型別化的容器(例如 Neo4jContainerRabbitMQContainer),這種方法就有效。如果您使用 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 判斷使用哪個容器映象,因此必須使用 @ServiceConnectionname 屬性來提供該提示。

您還可以使用 @ServiceConnectionname 屬性來覆蓋將使用的連線詳細資訊,例如在使用自定義映象時。如果您使用 Docker 映象 registry.mycompany.com/mirror/myredis,您將使用 @ServiceConnection(name="redis") 來確保建立 RedisConnectionDetails

使用服務連線的 SSL

您可以在支援的容器上使用 @Ssl@JksKeyStore@JksTrustStore@PemKeyStore@PemTrustStore 註解來啟用該服務連線的 SSL 支援。請注意,您仍然需要在 Testcontainer 內部執行的服務上自行啟用 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 註解將客戶端證書和金鑰載入到金鑰庫中,並使用 @PemTrustStore 註解將 CA 證書載入到信任庫中。這將對客戶端進行伺服器端認證,並且信任庫中的 CA 證書確保伺服器證書有效且受信任。

此示例中的 SecureRedisContainerRedisContainer 的一個自定義子類,它將證書複製到正確的位置,並使用命令列引數呼叫 redis-server 以啟用 SSL。

以下服務連線支援 SSL 註解

  • Cassandra

  • Couchbase

  • Elasticsearch

  • Kafka

  • MongoDB

  • RabbitMQ

  • Redis

ElasticsearchContainer 還支援伺服器端 SSL 的自動檢測。要使用此功能,請使用 @Ssl 註解容器,如以下示例所示,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.data.elasticsearch.test.autoconfigure.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 是一種稍微冗長但更靈活的替代方案。靜態 @DynamicPropertySource 方法允許向 Spring Environment 新增動態屬性值。

  • Java

  • Kotlin

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

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.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import org.testcontainers.neo4j.Neo4jContainer

@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 進行通訊。

© . This site is unofficial and not affiliated with VMware.