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 中提供:
连接详细信息 | 匹配条件 |
---|---|
名为 "symptoma/activemq" 的容器或 |
|
类型为 |
|
类型为 |
|
类型为 |
|
类型为 |
|
类型为 |
|
类型为 |
|
类型为 |
|
类型为 |
|
类型为 |
|
类型为 |
|
名为 "otel/opentelemetry-collector-contrib" 的容器或类型为 |
|
名为 "otel/opentelemetry-collector-contrib" 的容器或类型为 |
|
名为 "otel/opentelemetry-collector-contrib" 的容器或类型为 |
|
类型为 |
|
类型为 |
|
类型为 |
|
类型为 |
|
名为 "openzipkin/zipkin" 的容器 |
默认情况下,将为给定的 如果你只想创建适用类型的子集,可以使用 |
默认情况下,使用 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
。
动态属性
服务连接的一个稍微更冗长但更灵活的替代方案是 @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 通信。