3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-06-26

やりたいこと

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. です。
古い記事だと、古い記載のままでうまく動かなくてハマるので注意しましょう。

情報元: Redis Properties

テストを記述する

    @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" とする必要があります。

この辺の情報は以下の表に記載があります。
image.png
情報元: 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 の使い方でした。

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?