Testcontainers

Testcontainers 库提供了一种管理在 Docker 容器内运行的服务的方法。 它与 JUnit 集成,允许你编写一个测试类,可以在任何测试运行之前启动容器。 Testcontainers 特别适用于编写与真实后端服务(如 MySQL、MongoDB、Cassandra 等)通信的集成测试。

在以下章节中,我们将描述一些可以将 Testcontainers 与测试集成的方法。

使用 Spring Beans

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

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

  • 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 结合使用:

  • 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 的一个常见模式是在接口中声明容器实例作为静态字段。

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

  • 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

  • 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 作为 beans 管理时,它们的生命周期由 Spring 管理:

  • 容器 beans 在所有其他 beans 之前创建和启动。

  • 容器 beans 在所有其他 beans 销毁后停止。

这个过程确保任何依赖容器提供的功能的 beans 都可以使用这些功能。 它还确保在容器仍然可用时进行清理。

提示:当你的应用程序 beans 依赖容器的功能时,最好将容器配置为 Spring beans 以确保正确的生命周期行为。

注意:由 Testcontainers 而不是作为 Spring beans 管理容器不能保证 beans 和容器的关闭顺序。 容器可能会在依赖容器功能的 beans 清理之前关闭。 这可能导致客户端 beans 抛出异常,例如,由于连接丢失。

容器 beans 由 Spring 的 TestContext Framework 管理的每个应用程序上下文创建和启动一次。 有关 TestContext Framework 如何管理底层应用程序上下文和其中的 beans 的详细信息,请参考 Spring Framework 文档

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

注意:单个测试容器实例可以并且经常在多个测试类的测试执行过程中保留。

服务连接

服务连接是与任何远程服务的连接。 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,上述配置允许应用程序中的 Neo4j 相关 beans 与在 Testcontainers 管理的 Docker 容器内运行的 Neo4j 通信。 这是通过自动定义一个 Neo4jConnectionDetails bean 来实现的,然后由 Neo4j 自动配置使用,覆盖任何与连接相关的配置属性。

注意:要使用 Testcontainers 的服务连接,你需要将 spring-boot-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

类型为 ClickHouseContainerMariaDBContainerMSSQLServerContainerMySQLContainer、https://javadoc.io/doc/org.testcontainers/oracle-free/1.21.0/org/testcontainers/OracleContainer.html[OracleContainer (free),role=apiref]、https://javadoc.io/doc/org.testcontainers/oracle-xe/1.21.0/org/testcontainers/oracle/OracleContainer.html[OracleContainer (XE),role=apiref] 或 PostgreSQLContainer 的容器

RabbitConnectionDetails

类型为 RabbitMQContainer 的容器

RedisConnectionDetails

类型为 RedisContainerRedisStackContainer 的容器,或名为 "redis"、"redis/redis-stack" 或 "redis/redis-stack-server" 的容器

ZipkinConnectionDetails

名为 "openzipkin/zipkin" 的容器

默认情况下,将为给定的 Container 创建所有适用的连接详细信息 beans。 例如,一个 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 的自定义子类,它将证书复制到正确的位置并使用启用 SSL 的命令行参数调用 redis-server

以下服务连接支持 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() {
		// ...
	}

}

动态属性

服务连接的一个稍微更冗长但更灵活的替代方案是 @DynamicPropertySource。 静态 @DynamicPropertySource 方法允许向 Spring Environment 添加动态属性值。

  • 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 相关 beans 与在 Testcontainers 管理的 Docker 容器内运行的 Neo4j 通信。