やりたいこと
Spring Boot (>= 3.1) から Redis にアクセスして読み書きをする機能を実装する。
そのテスト環境を構築する。
テスト実行時のRedisは、TestContainers
機能を利用する。
Spring Bootが v3 になったのに伴い、変更点もあったのそれに対応する。
環境
Spring Boot: 3.1.0
TestContainers とは?
TestContainersは、Javaの JUnitテストでDockerコンテナを使うためのライブラリ。
Spring Bootテストにおいては、実際のデータベースやキューなどの外部リソースに依存する部分を、本番と同等の環境でテストするために使われます。
例えば、アプリケーションがPostgreSQLデータベースに依存している場合、ローカル環境にPostgreSQLがインストールされていないと、テストを実行できない問題が発生します。
しかし、TestContainersを用いると、 テスト実行時にDockerコンテナ上に一時的にPostgreSQL環境を構築し、その環境に対してテストを実行することができます。
テストが終了すれば自動的にそのコンテナは削除されるため、テストによる環境への影響を最小限に抑えることができます。
TestContainersはDockerを利用するので、テストを実行するマシンに Dockerがインストールされている必要があります。
Dependencyを設定する
Gradle環境では以下のように設定します。Maven使いの方は適宜読み替えてください。
dependencies {
// SpringからRedisにアクセスするのに必要
implementation("org.springframework.boot:spring-boot-starter-data-redis")
// Testcontainers を利用するのに必要
testImplementation("org.testcontainers:junit-jupiter")
}
Docker Daemonを起動する
TestContainers はテストを実行するPCにDockerが入っている必要があります。
Docker Desktopを使っている方は、Docker Desktopを起動して、Docker Daemonを起動しましょう。
# Docker daemonが動いていることを確認
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
注意: Mac環境でDocker Daemonにアクセスできない問題の解決
TestContainersの機能を使おうとした際に、Dockerコンテナの起動がうまくできないことがMac環境ではあります。
その場合は以下のようにリンクを作っておいてください。
$ sudo ln -s $HOME/.docker/run/docker.sock /var/run/docker.sock
どうも、TestContainersは、$HOME/.docker/run/docker.sock
を使ってDocker Deamonにアクセスしようとするようなのですが、Mac環境だとここに何も存在しないケースがあります。
なので、上記のようなリンクを使って、アクセスできるようにしてやります。
情報元: Test Container test cases are failing due to "Could not find a valid Docker environment"
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.
です。
古い記事だと、古い記載のままでうまく動かなくてハマるので注意しましょう。
テストを記述する
@Test
fun test0() {
redisTemplate.opsForValue().set("hello", "WORLD!!!")
val data = redisTemplate.opsForValue().get("hello")
assertEquals("WORLD!!!", data)
}
そして、テストを書けばOKです。
実際に、このテストを実行するとRedisのコンテナが作成しれて、テスト終了後には破棄されます。
Docker Desktopでコンテナ一覧を眺めていると、一瞬ですがRedisコンテナが作成されているところを見ることができました。(1秒くらいで破棄される)
TestContainers の設定を簡潔にする
Spring Boot 3.1以上では、ServiceConnectionという機能が利用できます。
これを使うと、 TestContainers の設定を簡潔に書くことができます。
この機能を使うには、Dependencyに以下の内容を追記する必要があります。
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"
とする必要があります。
この辺の情報は以下の表に記載があります。
情報元: Service Connections
これを見るとわかるように、RDBなどの他の種類のデータベースでは、それ専用のContainerがありますので、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
の方が好ましいです。
上記の設定でも同じように動くはずです。
まとめ
以上、Redisコンテナをテストのために一時的に立ち上げる、TestContainers の使い方でした。