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.factories 的 ContainerConnectionDetailsFactory 類處理。ContainerConnectionDetailsFactory 可以根據特定的 Container 子類或 Docker 映象名稱建立 ConnectionDetails Bean。
spring-boot-testcontainers jar 中提供了以下服務連線工廠
|
預設情況下,將為給定的 如果您只想建立適用型別的一個子集,可以使用 |
預設情況下,使用 Container.getDockerImageName().getRepository() 獲取用於查詢連線詳細資訊的名稱。Docker 映象名稱的儲存庫部分忽略任何登錄檔和版本。只要 Spring Boot 能夠獲取 Container 例項,這種方法就有效,就像上面示例中使用的 static 欄位一樣。
如果您正在使用 @Bean 方法,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 的 name 屬性來提供該提示。
您還可以使用 @ServiceConnection 的 name 屬性來覆蓋將使用的連線詳細資訊,例如在使用自定義映象時。如果您使用 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 證書確保伺服器證書有效且受信任。
此示例中的 SecureRedisContainer 是 RedisContainer 的一個自定義子類,它將證書複製到正確的位置,並使用命令列引數呼叫 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 進行通訊。