使用TestContainers轻松构建Spring Boot 3.1 + Redis测试环境
我想做的事情
使用Spring Boot(>= 3.1)来实现与Redis的读写功能。构建测试环境。测试运行时使用TestContainers功能访问Redis。
随着Spring Boot升级至v3版本,相应的变更点也产生了,并且需要进行适配。
环境
春季引导:3.1.0
TestContainers 是什么?
TestContainers是一个用于在Java的JUnit测试中使用Docker容器的库。
在Spring Boot测试中,它被用来测试依赖于实际数据库、队列等外部资源的部分,以便在生产环境中进行相同环境的测试。
例如,如果应用程序依赖于PostgreSQL数据库,那么如果本地环境没有安装PostgreSQL,将无法执行测试,这将导致问题。
然而,使用TestContainers,我们可以在测试运行时临时构建一个基于Docker容器的PostgreSQL环境,并在该环境中执行测试。
一旦测试结束,该容器将自动删除,因此可以将对环境造成的影响最小化。
由于TestContainers使用Docker,所以在执行测试时需要在测试机器上安装Docker。
設置依存関係
在Gradle环境中进行以下配置。如果您是使用Maven的话,请相应地进行更改。
dependencies {
// SpringからRedisにアクセスするのに必要
implementation("org.springframework.boot:spring-boot-starter-data-redis")
// Testcontainers を利用するのに必要
testImplementation("org.testcontainers:junit-jupiter")
}
启动Docker守护程序
使用TestContainers需要在执行测试的计算机上安装了Docker。如果您正在使用Docker Desktop,请启动Docker Desktop并运行Docker Daemon。
# Docker daemonが動いていることを確認
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
注意:解决在Mac环境下无法访问Docker Daemon的问题。
在Mac环境中,当尝试使用TestContainers功能时,可能无法成功启动Docker容器。
如果遇到这种情况,请事先创建以下链接。
$ sudo ln -s $HOME/.docker/run/docker.sock /var/run/docker.sock
您好,TestContainers似乎正在尝试通过$HOME/.docker/run/docker.sock访问Docker守护进程,但在Mac环境中这里可能为空。因此,我们将使用上述的链接来确保可以进行访问。
情报来源:由于“找不到有效的Docker环境”,测试容器测试用例失败。
将TestContainers的配置写入测试代码中。
package com.example.app.repository
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import org.testcontainers.utility.DockerImageName
@SpringBootTest
@Testcontainers
class SomeRepositoryTest {
@Autowired
lateinit var redisTemplate: RedisTemplate<String, String>
companion object {
@Container
val redis = GenericContainer(DockerImageName.parse("redis:5.0.3-alpine"))
.withExposedPorts(6379)
@DynamicPropertySource
@JvmStatic
fun redisProperties(registry: DynamicPropertyRegistry) {
registry.add("spring.data.redis.host", redis::getContainerIpAddress)
registry.add("spring.data.redis.port") { redis.getMappedPort(6379) }
}
}
}
我们逐个进行查看。
设定所需的注释。
// Spring Boot を有効にして、Autowiredが使えるようにする
@SpringBootTest
// Testcontainers を有効にする
@Testcontainers
获取 RedisTemplate 实例
// Autowired によって、RedisTemplateインスタンスを取得する
@Autowired
lateinit var redisTemplate: RedisTemplate<String, String>
这将在稍后用于对Redis进行读写操作。
进行 Testcontainers 的设置
companion object {
// 起動するRedisのDockerコンテナの情報を取得する
// 利用するDockerイメージを `redis:5.0.3-alpine` とする
@Container
val redis = GenericContainer(DockerImageName.parse("redis:5.0.3-alpine"))
// Port の設定をする
.withExposedPorts(6379)
// 動的にプロパティ情報を設定する
@DynamicPropertySource
@JvmStatic
fun redisProperties(registry: DynamicPropertyRegistry) {
// IP Address, Port をコンテナから取得した情報の通り、設定する
registry.add("spring.data.redis.host", redis::getContainerIpAddress)
registry.add("spring.data.redis.port") { redis.getMappedPort(6379) }
}
}
通过使用 @DynamicPropertySource,可以确保容器信息在每次启动时发生变化时能够对其进行适应。
如果在 application.properties 中进行设置,只能处理固定值,因此需要使用 @DynamicPropertySource。
请注意
registry.add("spring.data.redis.host", redis::getContainerIpAddress)
registry.add("spring.data.redis.port") { redis.getMappedPort(6379) }
在这里设置的是 spring.data.redis. ,但在 Spring Boot v3 中变为这样了。
如果使用旧版本,则是 spring.redis. 。
要注意的是,旧的文章可能保持旧的写法而无法正常工作,所以要小心。
消息來源:Redis 屬性
写测试
@Test
fun test0() {
redisTemplate.opsForValue().set("hello", "WORLD!!!")
val data = redisTemplate.opsForValue().get("hello")
assertEquals("WORLD!!!", data)
}
执行测试后,在Docker Desktop中可以看到Redis容器被创建并且在测试结束后被销毁(大约在1秒内销毁)。
简化TestContainers的配置
在Spring Boot 3.1及以上版本中,可以使用ServiceConnection功能。使用该功能可以简化TestContainers的配置。
要使用此功能,需要在依赖中添加以下内容。
dependencies {
...(略)...
// Spring v3.1 で入った機能を使うのに必要。
testImplementation("org.springframework.boot:spring-boot-testcontainers")
...(略)...
}
然后,按照以下方式在一个合适的文件中创建Configuration。
@TestConfiguration
class TestContainerConfig {
@Bean
@ServiceConnection(name = "redis")
fun redisContainer(): GenericContainer<*> {
return GenericContainer(DockerImageName.parse("redis:5.0.3-alpine"))
.withExposedPorts(6379)
}
}
在使用Redis时需要注意的是,
@ServiceConnection(name = "redis")
需要将 name 设置为 “redis”。
从这个例子可以看出,在关系型数据库(RDB)和其他类型的数据库中,都有各自专用的容器,因此不需要使用通用容器(GenericContainer)。
通过这样做,可以将测试代码简化为以下方式。
@SpringBootTest
@Testcontainers
@Import(TestContainerConfig::class)
class SomeRepositoryTest {
@Autowired
lateinit var redisTemplate: RedisTemplate<String, String>
// 以下の内容を省略できるようになる
// companion object {
// @Container
// val redis = GenericContainer(DockerImageName.parse("redis:5.0.3-alpine"))
// .withExposedPorts(6379)
//
// @DynamicPropertySource
// @JvmStatic
// fun redisProperties(registry: DynamicPropertyRegistry) {
// registry.add("spring.data.redis.host", redis::getContainerIpAddress)
// registry.add("spring.data.redis.port") { redis.getMappedPort(6379) }
// }
// }
@Test
fun test0() {
redisTemplate.opsForValue().set("hello", "WORLD!!!")
val data = redisTemplate.opsForValue().get("hello")
assertEquals("WORLD!!!", data)
}
}
简单解释一下的话,
@Import(TestContainerConfig::class)
为了导入刚刚创建的带有 @TestConfiguration 标注的 TestContainerConfig 类的设置,这是必要的。
对于 @Configuration,设置会自动导入,但对于 @TestConfiguration,需要明确使用 @Import 导入,请注意。
由于这样做可以降低意外导入不必要设置的风险,因此更推荐使用 @TestConfiguration。
即使采用上述设置,应该也会以相同方式运作。
总结
以上就是使用TestContainers临时启动Redis容器进行测试的方法。