Testcontainers

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

在以下部分中,我们将描述一些你可以用来将 Testcontainers 与测试集成的方法。

使用 Spring Beans

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

要将容器声明为 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 作为 bean 管理时,它们的生命周期由 Spring 管理:

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

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

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

当你的应用程序 bean 依赖于容器的功能时,最好将容器配置为 Spring bean 以确保正确的生命周期行为。
由 Testcontainers 而不是作为 Spring bean 管理容器不能保证 bean 和容器的关闭顺序。 容器可能在依赖于容器功能的 bean 清理之前关闭。 这可能导致客户端 bean 抛出异常,例如,由于连接丢失。

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

容器 bean 作为 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 相关 bean 与在 Testcontainers 管理的 Docker 容器中运行的 Neo4j 通信。 这是通过自动定义一个 Neo4jConnectionDetails 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 的容器

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.20.6/org/testcontainers/OracleContainer.html[OracleContainer (free),role=apiref]、https://javadoc.io/doc/org.testcontainers/oracle-xe/1.20.6/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 创建所有适用的连接详细信息 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

动态属性

服务连接的一个稍微更冗长但更灵活的替代方案是 @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 相关 bean 与在 Testcontainers 管理的 Docker 容器中运行的 Neo4j 通信。