跳到主要内容
版本:3.5.10

Testcontainers

QWen Max 中英对照 Testcontainers

Testcontainers 库提供了一种管理在 Docker 容器内运行的服务的方式。它与 JUnit 集成,允许你编写一个测试类,在任何测试运行之前启动一个容器。Testcontainers 在编写与真实后端服务(如 MySQL、MongoDB、Cassandra 等)交互的集成测试时尤其有用。

在以下章节中,我们将介绍一些可用于将 Testcontainers 集成到测试中的方法。

使用 Spring Beans

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

要将一个容器声明为 bean,请在你的测试配置中添加一个 @Bean 方法:

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.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() {
...
}

}
提示

这种管理容器的方法通常与 服务连接注解 结合使用。

使用 JUnit 扩展

Testcontainers 提供了一个 JUnit 扩展,可用于在测试中管理容器。通过在测试类上应用 Testcontainers 的 @Testcontainers 注解即可激活该扩展。

然后,你可以在静态容器字段上使用 @Container 注解。

[@Testcontainers](https://javadoc.io/doc/org.testcontainers/junit-jupiter/1.21.4/org/testcontainers/junit/jupiter/Testcontainers.html) 注解可用于普通的 JUnit 测试,也可以与 [@SpringBootTest](https://docs.spring.io/spring-boot/3.5.10/api/java/org/springframework/boot/test/context/SpringBootTest.html) 结合使用:

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() {
...
}

}

上面的示例将在运行任何测试之前启动一个 Neo4j 容器。该容器实例的生命周期由 Testcontainers 管理,如 其官方文档 中所述。

备注

在大多数情况下,你还需要额外配置应用程序,以连接到容器中运行的服务。

导入容器配置接口

Testcontainers 的一种常见模式是将容器实例声明为接口中的静态字段。

例如,以下接口声明了两个容器,一个名为 mongo,类型为 MongoDBContainer,另一个名为 neo4j,类型为 Neo4jContainer

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");

}

当你以这种方式声明容器时,可以通过让测试类实现该接口,在多个测试中重用它们的配置。

也可以在你的 Spring Boot 测试中使用相同的接口配置。为此,请在你的测试配置类上添加 @ImportTestcontainers

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 Framework 文档

容器 Bean 会作为 TestContext Framework 标准应用上下文关闭流程的一部分而被停止。当应用上下文关闭时,这些容器也会一并关闭。这通常发生在所有使用该特定缓存应用上下文的测试执行完毕之后。根据 TestContext Framework 中配置的缓存行为,这种情况也可能更早发生。

备注

单个测试容器实例可以(并且通常会)在多个测试类的测试执行过程中被保留。

Service Connections

服务连接(service connection)是指到任意远程服务的连接。Spring Boot 的自动配置可以使用服务连接的详细信息,并利用这些信息建立到远程服务的连接。在此过程中,连接详细信息优先于任何与连接相关的配置属性。

使用 Testcontainers 时,可以通过在测试类中对容器字段添加注解,自动为运行在容器中的服务创建连接详细信息。

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() {
...
}

}

得益于 @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 中提供了以下服务连接工厂:

连接详情匹配于
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类型为 MongoDBContainer 的容器
Neo4jConnectionDetails类型为 Neo4jContainer 的容器
OtlpLoggingConnectionDetails名为 "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 镜像名称中的 repository 部分会忽略任何 registry 和版本信息。只要 Spring Boot 能够获取到 Container 的实例,这种方式就能正常工作,例如在上面的示例中使用 static 字段时就是这种情况。

如果你使用的是 @Bean 方法,Spring Boot 不会调用该 bean 方法来获取 Docker 镜像名称,因为这会导致提前初始化(eager initialization)问题。相反,Spring Boot 会使用该 bean 方法的返回类型来确定应使用哪个连接细节。只要你在使用类型化的容器(例如 Neo4jContainerRabbitMQContainer),这种方式就能正常工作。但如果你使用的是 GenericContainer,例如下面示例中用于 Redis 的情况,这种方式就不再有效:

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");
}

}

Spring Boot 无法从 GenericContainer 判断使用了哪个容器镜像,因此必须使用 @ServiceConnection 中的 name 属性来提供该提示。

你也可以使用 @ServiceConnectionname 属性来覆盖将要使用的连接详情,例如在使用自定义镜像时。如果你使用的是 Docker 镜像 registry.mycompany.com/mirror/myredis,你可以使用 @ServiceConnection(name="redis") 来确保创建 RedisConnectionDetails

SSL 与 Service Connections

你可以在受支持的容器上使用 @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 注解将客户端证书和密钥加载到 keystore 中,并使用 @PemTrustStore 注解将 CA 证书加载到 truststore 中。这将对客户端进行服务器端的身份验证,而 truststore 中的 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.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() {
// ...
}

}

动态属性

比服务连接(service connections)稍微冗长一些但更加灵活的替代方案是 @DynamicPropertySource。一个静态的 @DynamicPropertySource 方法允许将动态属性值添加到 Spring 的 Environment 中。

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);
}

}

上述配置允许应用程序中的 Neo4j 相关 Bean 与 Testcontainers 管理的 Docker 容器中运行的 Neo4j 进行通信。