Spring Boot 3.1 + Redis のテスト環境を簡単にTestContainersで構築

# やりたいこと
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使いの方は適宜読み替えてください。
```gradle
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を起動しましょう。

```bash
# Docker daemonが動いていることを確認
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS  PORTS  NAMES
```

# 注意：　Mac環境でDocker Daemonにアクセスできない問題の解決
TestContainersの機能を使おうとした際に、Dockerコンテナの起動がうまくできないことがMac環境ではあります。
その場合は以下のようにリンクを作っておいてください。

```bash
$ 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"](https://stackoverflow.com/questions/61108655/test-container-test-cases-are-failing-due-to-could-not-find-a-valid-docker-envi)


# TestContainers の設定をテストコードに記載する
```kotlin
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) }
        }
    }
}
```

一つずつ見ていきます。
### 必要なアノテーションの設定
```kotlin
// Spring Boot を有効にして、Autowiredが使えるようにする
@SpringBootTest
// Testcontainers を有効にする
@Testcontainers
```

### RedisTemplate インスタンスを取得する
```kotlin
    // Autowired によって、RedisTemplateインスタンスを取得する
    @Autowired
    lateinit var redisTemplate: RedisTemplate<String, String>
```
これは後で、Redisへの読み書きをする際に利用します。

### Testcontainers の設定をする
```kotlin
    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` を使う必要があります。


**注意**
```kotlin
            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 Properties](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide#redis-properties)

# テストを記述する
```kotlin
    @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に以下の内容を追記する必要があります。
```gradle
dependencies {
    ...(略)...
    // Spring v3.1 で入った機能を使うのに必要。
    testImplementation("org.springframework.boot:spring-boot-testcontainers")
    ...(略)...
}
```


そして、以下のようにConfigurationを適当なファイルに作成します。
```kotlin
@TestConfiguration
class TestContainerConfig {
    @Bean
    @ServiceConnection(name = "redis")
    fun redisContainer(): GenericContainer<*> {
        return GenericContainer(DockerImageName.parse("redis:5.0.3-alpine"))
            .withExposedPorts(6379)
    }
}
```

ここで気をつける点として、Redisを使う場合、
```kotlin
    @ServiceConnection(name = "redis")
```
のように、 `name = "redis"` とする必要があります。

この辺の情報は以下の表に記載があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181439/15b96507-4f14-5746-f478-e60df50c3615.png)
[情報元: Service Connections](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.testing.testcontainers.service-connections)

これを見るとわかるように、RDBなどの他の種類のデータベースでは、それ専用のContainerがありますので、`GenericContainer` を使う必要がありません。

こうすることでテストコードを以下のように簡略化できます。

```kotlin
@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)
    }
}
```

軽く解説すると、
```kotlin
@Import(TestContainerConfig::class)
```
こちらは先ほど作った、`@TestConfiguration` をつけた `TestContainerConfig` クラスの設定を取り込むために必要です。
`@Configuration` の場合は自動的に設定が取り込まれますが、 `@TestConfiguration` の場合は　`@Import` で明示的に取り込む必要がありますので注意してください。
その分、予期せず不要な設定が取り込まれるリスクも減るので、`@TestConfiguration` の方が好ましいです。

上記の設定でも同じように動くはずです。

# まとめ
以上、Redisコンテナをテストのために一時的に立ち上げる、TestContainers の使い方でした。