105_Spring AI 干货笔记之集成测试
本文介绍了Spring AI中Testcontainers的集成方案,用于在测试环境中管理容器化服务。通过spring-ai-spring-boot-testcontainers模块,可自动建立与模型服务或向量存储的连接。文章重点阐述了@ServiceConnection注解的使用,它能自动为容器创建连接详情,覆盖常规配置属性。同时展示了通过Spring Bean或JUnit扩展管理容器生命周期的
一、Testcontainers
Testcontainers 库提供了一种管理在 Docker 容器内运行的服務的方法。它与 JUnit 集成,允许您编写一个测试类,在任何测试运行之前启动一个容器。Testcontainers 对于编写与真实后端服务(如 MySQL、MongoDB、Cassandra 等)通信的集成测试特别有用。
在接下来的部分中,我们将描述一些可以用来将 Testcontainers 与您的测试集成的方法。
Spring AI 提供了 Spring Boot 自动配置,用于建立与通过 Testcontainers 运行的模型服务或向量存储的连接。要启用它,请将以下依赖项添加到项目的 Maven pom.xml 文件中:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-spring-boot-testcontainers</artifactId>
</dependency>
或添加到 Gradle build.gradle 构建文件中:
dependencies {
implementation 'org.springframework.ai:spring-ai-spring-boot-testcontainers'
}
请参阅 依赖管理 部分,将 Spring AI BOM 添加到您的构建文件中。
二、服务连接
spring-ai-spring-boot-testcontainers 模块提供了以下服务连接工厂:
更多服务连接由 Spring Boot 模块 spring-boot-testcontainers 提供。有关完整列表,请参阅 Testcontainers 服务连接文档页面。
三、使用 Spring Bean
Testcontainers 提供的容器可以被 Spring Boot 作为 bean 管理。
要将容器声明为 bean,请将 @Bean 方法添加到您的测试配置中:
Java
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"));
}
}
Kotlin
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"))
}
}
然后,您可以通过在测试类中导入配置类来注入并使用容器:
Java
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() {
...
}
}
Kotlin
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
lateinit var mongo: MongoDBContainer
@Test
fun myTest() {
...
}
}
这种管理容器的方法通常与服务连接注解结合使用。
四、使用 JUnit 扩展
Testcontainers 提供了一个 JUnit 扩展,可用于在测试中管理容器。该扩展通过在测试类上应用 Testcontainers 的 @Testcontainers 注解来激活。
然后您可以在静态容器字段上使用 @Container 注解。
@Testcontainers 注解可用于普通 JUnit 测试,或与 @SpringBootTest 结合使用:
Java
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() {
...
}
}
kotlin
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import org.testcontainers.neo4j.Neo4jContainer
@Testcontainers
@SpringBootTest
class MyIntegrationTests {
companion object {
@Container
val neo4j = Neo4jContainer("neo4j:5")
}
@Test
fun myTest() {
...
}
}
上面的示例将在任何测试运行之前启动一个 Neo4j 容器。容器实例的生命周期由 Testcontainers 管理,如其官方文档中所述。
在大多数情况下,您还需要配置应用程序以连接到在容器中运行的服务。
五、导入容器配置接口
使用 Testcontainers 的一个常见模式是将容器实例声明为接口中的静态字段。
例如,以下接口声明了两个容器,一个名为 mongo 类型为 MongoDBContainer,另一个名为 neo4j 类型为 Neo4jContainer:
Java
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");
}
Kotlin
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.mongodb.MongoDBContainer
import org.testcontainers.neo4j.Neo4jContainer
interface MyContainers {
companion object {
@JvmField
@Container
val mongoContainer = MongoDBContainer("mongo:5.0")
@JvmField
@Container
val neo4jContainer = Neo4jContainer<Nothing>("neo4j:5")
}
}
当您以这种方式声明容器时,可以通过让测试类实现该接口,在多个测试中重用它们的配置。
也可以在 Spring Boot 测试中使用相同的接口配置。为此,请将 @ImportTestcontainers 添加到您的测试配置类中:
Java
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.context.ImportTestcontainers;
@TestConfiguration(proxyBeanMethods = false)
@ImportTestcontainers(MyContainers.class)
class MyTestConfiguration {
}
Kotlin
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 框架的标准应用程序上下文关闭过程的一部分被停止。当应用程序上下文关闭时,容器也会关闭。这通常在使用该特定缓存应用程序上下文的所有测试执行完毕后发生。根据 TestContext 框架中配置的缓存行为,也可能更早发生。
单个测试容器实例可以并且经常跨多个测试类的执行而保留。
七、服务连接是与任何远程服务的连接
服务连接是与任何远程服务的连接。Spring Boot 的自动配置可以使用服务连接的详细信息,并利用它们建立与远程服务的连接。这样做时,连接详细信息优先于任何与连接相关的配置属性。
当使用 Testcontainers 时,可以通过在测试类中注解容器字段来自动为在容器中运行的服务创建连接详细信息。
Java
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() {
...
}
}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.testcontainers.service.connection.ServiceConnection
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import org.testcontainers.neo4j.Neo4jContainer
@Testcontainers
@SpringBootTest
class MyIntegrationTests {
companion object {
@Container
@ServiceConnection
val neo4j = Neo4jContainer("neo4j:5")
}
@Test
fun 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 中提供了以下服务连接工厂:

默认情况下,将为给定的 Container 创建所有适用的连接详情 bean。例如,PostgreSQLContainer 将同时创建 JdbcConnectionDetails 和 R2dbcConnectionDetails。
如果您只想创建适用类型的子集,可以使用 @ServiceConnection 的 type 属性。
默认情况下,使用 Container.getDockerImageName().getRepository() 来获取用于查找连接详情的名称。Docker 镜像名称的存储库部分忽略任何注册表和版本。只要 Spring Boot 能够获取 Container 的实例(如上面示例中使用静态字段的情况),这种方法就有效。
如果您使用 @Bean 方法,Spring Boot 不会调用 bean 方法来获取 Docker 镜像名称,因为这会导致 eager 初始化问题。相反,bean 方法的返回类型用于找出应使用哪个连接详情。只要您使用的是类型化容器,如 Neo4jContainer 或 RabbitMQContainer,这种方法就有效。如果您使用 GenericContainer,例如下面示例中与 Redis 一起使用,则此方法停止工作:
Java
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");
}
}
Kotlin
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<Nothing>("redis:7")
}
}
Spring Boot 无法从 GenericContainer 判断使用了哪个容器镜像,因此必须使用 @ServiceConnection 的 name 属性来提供该提示。
您还可以使用 @ServiceConnection 的 name 属性来覆盖将使用哪个连接详情,例如在使用自定义镜像时。如果您使用 Docker 镜像 registry.mycompany.com/mirror/myredis,则应使用 @ServiceConnection(name=“redis”) 以确保创建 DataRedisConnectionDetails。
八、带有服务连接的 SSL
您可以在受支持的容器上使用 @Ssl、@JksKeyStore、@JksTrustStore、@PemKeyStore 和 @PemTrustStore 注解,为该服务连接启用 SSL 支持。请注意,您仍然需要自行在 Testcontainer 内部运行的服务上启用 SSL,这些注解仅在您的应用程序中配置客户端 SSL。
Java
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() {
// ...
}
}
Kotlin
import com.redis.testcontainers.RedisContainer
import org.junit.jupiter.api.Test
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
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
@Testcontainers
@SpringBootTest
class MyRedisWithSslIntegrationTests {
companion object {
@Container
@ServiceConnection
@PemKeyStore(certificate = "classpath:client.crt", privateKey = "classpath:client.key")
@PemTrustStore("classpath:ca.crt")
val redis: RedisContainer = SecureRedisContainer("redis:latest")
}
@Autowired
lateinit var operations: RedisOperations<Any?, Any?>
@Test
fun testRedis() {
// ...
}
}
上面的代码使用 @PemKeyStore 注解将客户端证书和密钥加载到密钥库中,并使用 @PemTrustStore 注解将 CA 证书加载到信任库中。这将使客户端对服务器进行身份验证,并且信任库中的 CA 证书可确保服务器证书有效且受信任。
此示例中的 SecureRedisContainer 是 RedisContainer 的一个自定义子类,它将证书复制到正确的位置,并使用启用 SSL 的命令行参数调用 redis-server。
以下服务连接支持 SSL 注解:
-
Cassandra
-
Couchbase
-
Elasticsearch
-
Kafka
-
MongoDB
-
RabbitMQ
-
Redis
ElasticsearchContainer 额外支持自动检测服务器端 SSL。要使用此功能,请使用 @Ssl 注解容器,如以下示例所示,Spring Boot 会为您处理客户端 SSL 配置:
Java
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() {
// ...
}
}
Kotlin
import org.junit.jupiter.api.Test
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
import org.testcontainers.elasticsearch.ElasticsearchContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
@Testcontainers
@DataElasticsearchTest
class MyElasticsearchWithSslIntegrationTests {
companion object {
@Ssl
@Container
@ServiceConnection
val elasticsearch = ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:8.17.2")
}
@Autowired
lateinit var elasticsearchTemplate: ElasticsearchTemplate
@Test
fun testElasticsearch() {
// ...
}
}
九、动态属性
服务连接的一个稍显冗长但也更灵活的替代方案是 @DynamicPropertySource。一个静态的 @DynamicPropertySource 方法允许将动态属性值添加到 Spring 环境中。
Java
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);
}
}
Kotlin
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 {
companion object {
@Container
val neo4j = Neo4jContainer("neo4j:5")
}
@Test
fun myTest() {
// ...
}
@DynamicPropertySource
fun neo4jProperties(registry: DynamicPropertyRegistry) {
registry.add("spring.neo4j.uri", neo4j::getBoltUrl)
}
}
上述配置允许应用程序中与 Neo4j 相关的 bean 与运行在 Testcontainers 管理的 Docker 容器中的 Neo4j 通信。
更多推荐



所有评论(0)