此版本仍在开发中,尚未视为稳定版。如需最新稳定版本,请使用 Spring Boot 4.0.4spring-doc.cadn.net.cn

Testcontainers

Testcontainers 库提供了一种管理在 Docker 容器内运行的服务的方式。 它与 JUnit 集成,允许你编写一个测试类,在任何测试运行之前启动一个容器。 Testcontainers 特别适用于编写与真实后端服务(如 MySQL、MongoDB、Cassandra 等)进行交互的集成测试。spring-doc.cadn.net.cn

在以下章节中,我们将介绍一些可用于将 Testcontainers 与您的测试集成的方法。spring-doc.cadn.net.cn

使用 Spring Beans

Testcontainers 提供的容器可以由 Spring Boot 作为 Bean 进行管理。spring-doc.cadn.net.cn

要将容器声明为 Bean,请在您的测试配置中添加一个 @Bean 方法:spring-doc.cadn.net.cn

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

}

然后,您可以通过在测试类中导入配置类来注入并使用该容器:spring-doc.cadn.net.cn

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 注解,即可激活该扩展。spring-doc.cadn.net.cn

然后,您可以在静态容器字段上使用 @Container 注解。spring-doc.cadn.net.cn

@Testcontainers 注解可用于原生 JUnit 测试,或与 @SpringBootTest 结合使用:spring-doc.cadn.net.cn

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 管理,如其官方文档中所述。spring-doc.cadn.net.cn

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

导入容器配置接口

使用 Testcontainers 的一种常见模式是将容器实例声明为接口中的静态字段。spring-doc.cadn.net.cn

例如,以下接口声明了两个容器,一个名为 mongo,类型为 MongoDBContainer,另一个名为 neo4j,类型为 Neo4jContainerspring-doc.cadn.net.cn

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-doc.cadn.net.cn

在 Spring Boot 测试中也可以使用相同的接口配置。 为此,请将 @ImportTestcontainers 添加到您的测试配置类中:spring-doc.cadn.net.cn

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-doc.cadn.net.cn

当容器由 Spring 作为 Bean 进行管理时,它们的生命周期就由 Spring 来管理:spring-doc.cadn.net.cn

该过程确保了任何依赖于容器所提供功能的 Bean 都可以使用这些功能。 同时,它还确保在容器仍然可用时对这些 Bean 进行清理。spring-doc.cadn.net.cn

当您的应用程序 Bean 依赖于容器的功能时,建议将这些容器配置为 Spring Bean,以确保其生命周期行为正确。
由 Testcontainers 管理的容器(而非作为 Spring Bean)无法保证 Bean 和容器关闭的顺序。 可能会出现容器在依赖其功能的 Bean 完成清理之前就被关闭的情况。 这可能导致客户端 Bean 抛出异常,例如由于连接丢失而引发的异常。

容器 Bean 由 Spring 的 TestContext 框架管理,每个应用程序上下文仅创建并启动一次。 有关 TestContext 框架如何管理底层应用程序上下文及其内部 Bean 的详细信息,请参阅Spring Framework 文档spring-doc.cadn.net.cn

容器 Bean 会在 TestContext 框架的标准应用上下文关闭过程中被停止。 当应用上下文关闭时,这些容器也会一并关闭。 这通常发生在所有使用该特定缓存应用上下文的测试执行完毕之后。 根据 TestContext 框架中配置的缓存行为,这种情况也可能更早发生。spring-doc.cadn.net.cn

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

服务连接

服务连接是指与任何远程服务的连接。 Spring Boot 的自动配置可以消费服务连接的详细信息,并利用这些信息建立与远程服务的连接。 在此过程中,连接细节优先于任何相关的配置属性。spring-doc.cadn.net.cn

使用 Testcontainers 时,可以通过在测试类中为容器字段添加注解,自动为运行在容器中的服务创建连接信息。spring-doc.cadn.net.cn

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-doc.cadn.net.cn

为了在 Testcontainers 中使用服务连接,你需要将 spring-boot-testcontainers 模块作为测试依赖项添加进来。

服务连接注解由通过 spring.factories 注册的 ContainerConnectionDetailsFactory 类进行处理。 ContainerConnectionDetailsFactory 可以基于特定的 Container 子类或 Docker 镜像名称创建 ConnectionDetails Bean。spring-doc.cadn.net.cn

以下服务连接工厂包含在 spring-boot-testcontainers jar 包中:spring-doc.cadn.net.cn

连接详细信息 匹配于

ActiveMQConnectionDetailsspring-doc.cadn.net.cn

名为"symptoma/activemq"的容器或 ActiveMQContainerspring-doc.cadn.net.cn

ArtemisConnectionDetailsspring-doc.cadn.net.cn

类型为 ArtemisContainer 的容器spring-doc.cadn.net.cn

CassandraConnectionDetailsspring-doc.cadn.net.cn

类型为 CassandraContainer 的容器spring-doc.cadn.net.cn

CouchbaseConnectionDetailsspring-doc.cadn.net.cn

类型为 CouchbaseContainer 的容器spring-doc.cadn.net.cn

ElasticsearchConnectionDetailsspring-doc.cadn.net.cn

类型为 ElasticsearchContainer 的容器spring-doc.cadn.net.cn

FlywayConnectionDetailsspring-doc.cadn.net.cn

类型为 JdbcDatabaseContainer 的容器spring-doc.cadn.net.cn

JdbcConnectionDetailsspring-doc.cadn.net.cn

类型为 JdbcDatabaseContainer 的容器spring-doc.cadn.net.cn

KafkaConnectionDetailsspring-doc.cadn.net.cn

类型为 KafkaContainerConfluentKafkaContainerRedpandaContainer 的容器spring-doc.cadn.net.cn

LdapConnectionDetailsspring-doc.cadn.net.cn

名为"osixia/openldap"的容器或类型为 LLdapContainer 的容器spring-doc.cadn.net.cn

LiquibaseConnectionDetailsspring-doc.cadn.net.cn

类型为 JdbcDatabaseContainer 的容器spring-doc.cadn.net.cn

MongoConnectionDetailsspring-doc.cadn.net.cn

类型为 MongoDBContainerMongoDBAtlasLocalContainer 的容器spring-doc.cadn.net.cn

Neo4jConnectionDetailsspring-doc.cadn.net.cn

类型为 Neo4jContainer 的容器spring-doc.cadn.net.cn

OtlpLoggingConnectionDetailsspring-doc.cadn.net.cn

名为"otel/opentelemetry-collector-contrib"的容器或类型为 LgtmStackContainer 的容器spring-doc.cadn.net.cn

OtlpMetricsConnectionDetailsspring-doc.cadn.net.cn

名为"otel/opentelemetry-collector-contrib"的容器或类型为 LgtmStackContainer 的容器spring-doc.cadn.net.cn

OtlpTracingConnectionDetailsspring-doc.cadn.net.cn

名为"otel/opentelemetry-collector-contrib"的容器或类型为 LgtmStackContainer 的容器spring-doc.cadn.net.cn

PulsarConnectionDetailsspring-doc.cadn.net.cn

类型为 PulsarContainer 的容器spring-doc.cadn.net.cn

R2dbcConnectionDetailsspring-doc.cadn.net.cn

类型为 ClickHouseContainerMariaDBContainerMSSQLServerContainerMySQLContainerOracleContainer(免费版)OracleContainer(XE 版)PostgreSQLContainer的容器spring-doc.cadn.net.cn

RabbitConnectionDetailsspring-doc.cadn.net.cn

类型为 RabbitMQContainer 的容器spring-doc.cadn.net.cn

RabbitStreamConnectionDetailsspring-doc.cadn.net.cn

@ServiceConnectiontype 属性包含 RabbitStreamConnectionDetails 时,类型为 RabbitMQContainer 的容器spring-doc.cadn.net.cn

DataRedisConnectionDetailsspring-doc.cadn.net.cn

类型为 RedisContainerRedisStackContainer 的容器,或名为 "redis"、"redis/redis-stack" 或 "redis/redis-stack-server" 的容器spring-doc.cadn.net.cn

ZipkinConnectionDetailsspring-doc.cadn.net.cn

名为“openzipkin/zipkin”的容器spring-doc.cadn.net.cn

默认情况下,除了 RabbitStreamConnectionDetails 之外,将为给定的 Container 创建所有适用的连接详情 Bean。 例如,PostgreSQLContainer 将同时创建 JdbcConnectionDetailsR2dbcConnectionDetailsspring-doc.cadn.net.cn

如果您只想创建适用类型的子集,可以使用 @ServiceConnectiontype 属性。spring-doc.cadn.net.cn

要从 RabbitMQContainer 创建 RabbitStreamConnectionDetails Bean,您必须使用 @ServiceConnectiontype 属性进行启用。 容器还必须暴露端口 5552,即 RabbitMQ 流端口。spring-doc.cadn.net.cn

默认情况下,使用 Container.getDockerImageName().getRepository() 来获取用于查找连接详情的名称。 Docker 镜像名称的仓库部分会忽略任何注册表和版本信息。 只要 Spring Boot 能够获取 Container 的实例,此方式即可正常工作;当使用如上述示例中的 static 字段时,通常就是这种情况。spring-doc.cadn.net.cn

如果您使用的是 @Bean 方法,Spring Boot 将不会调用该 Bean 方法来获取 Docker 镜像名称,因为这会导致提前初始化的问题。 相反,系统会使用 Bean 方法的返回类型来确定应使用哪种连接详情。 只要您使用的是类型化的容器(例如 Neo4jContainerRabbitMQContainer),此机制即可正常工作。 但如果您使用的是 GenericContainer(如下面以 Redis 为例所示),则此机制将不再生效:spring-doc.cadn.net.cn

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 属性来提供该提示。spring-doc.cadn.net.cn

您还可以使用 @ServiceConnectionname 属性来覆盖将使用的连接详细信息,例如在使用自定义镜像时。 如果您正在使用 Docker 镜像 registry.mycompany.com/mirror/myredis,则应使用 @ServiceConnection(name="redis") 以确保创建 DataRedisConnectionDetailsspring-doc.cadn.net.cn

使用服务连接配置 SSL

您可以在支持的容器上使用 @Ssl@JksKeyStore@JksTrustStore@PemKeyStore@PemTrustStore 注解,以启用对该服务连接的 SSL 支持。 请注意,您仍需在运行于 Testcontainer 内部的服务上自行启用 SSL;这些注解仅配置应用程序客户端一侧的 SSL。spring-doc.cadn.net.cn

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 证书可确保服务器证书有效且受信任。spring-doc.cadn.net.cn

本例中的 SecureRedisContainerRedisContainer 的一个自定义子类,它会将证书复制到正确的位置,并通过命令行参数调用 redis-server 以启用 SSL。spring-doc.cadn.net.cn

SSL 注解支持以下服务连接:spring-doc.cadn.net.cn

ElasticsearchContainer 还支持自动检测服务器端 SSL。 要使用此功能,请使用 @Ssl 注解容器,如下例所示,Spring Boot 将为您处理客户端 SSL 配置:spring-doc.cadn.net.cn

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 环境中。spring-doc.cadn.net.cn

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 进行通信。spring-doc.cadn.net.cn