Testcontainersで立てたRedisにSpring Bootアプリからアクセスする

Spring

JUnitコードから外部リソースにアクセスするときに何を使うかって結構悩む人も多いんじゃないかと思いますが、最近お仕事で教えてもらったTestcontainersというのが何かと便利なようなのでRedisへのアクセスを例に簡単に紹介してみようと思います。

Testcontainersとは

Introduction · Testcontainers

簡単に言えばテストコードからDockerイメージを起動してくれる便利ライブラリです。もちろんテストが終わったらコンテナは片付けてくれます。

テストコードからの外部リソースアクセスといえば何はなくともデータベースな気もしますが、今回はRedisを例に説明していきます。

データベースアクセスに関しては需要があるだけに結構作り込まれていて、コンフィグの設定や初期化スクリプトの流し方などなど結構凝った事ができ(るので結構難しかったりす)るのでまた別の機会にでも。

前提バージョン

この記事で使用した各種ライブラリのバージョンは以下の通り。

  • Spring Boot:2.1.0.RELEASE
  • kotlin:1.2.70
  • Java:1.8.0_181
  • junit-jupiter-api:5.3.1
  • Gradle:4.10.1
  • testcontainers:1.10.1
  • Docker CE:18.09.0

やりたいこと

今回は簡単です。

  • JUnitコードの中でRedisのDockerイメージを起動させる
  • テスト対象のSpring BootアプリケーションのRedisTemplateからアクセスする

build.gradle

とりあえず依存関係とJUnit5用の設定。

dependencies {
    // 省略
    // Redis
    implementation('org.springframework.boot:spring-boot-starter-data-redis')
    // JUnit
    testCompile('org.springframework.boot:spring-boot-starter-test') {
        exclude module: 'junit'
    }
    testImplementation('org.junit.jupiter:junit-jupiter-api')
    testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine')
    // Testcontainers
    testImplementation('org.testcontainers:junit-jupiter:1.10.1')
    // 省略
}

// JUnit
test {
    useJUnitPlatform {
        includeEngines 'junit-jupiter'
    }
}

spring-boot-starter-testのデフォルトがJUnit4だったりするのでなんかそのまま使っちゃったりしてなかなかJUnit5に馴染まなかったりもするのですが。。。今回もなんとなく5で動かしただけで5らしいテストは一つも書いてないんですけどね(笑)

JUnitコードからRedisを立てる

一応公式リファレンスにコード例があり。

Benefits · Testcontainers

引用すると、

@ClassRule
public static GenericContainer redis = new GenericContainer("redis:3.0.2").withExposedPorts(6379);

イメージ名とポート番号を指定しなさいと。@ClassRuleはJUnit4のお作法ですがまぁその辺はよしなにということで。

ふむふむ簡単だ・・・ん?

getMappedPort(...) returns the Docker mapped port for a port that has been exposed on the container

Oh(・´з`・)

つまりどういうことかと言うと、何番ポートでアクセスすればいいかは起動した後に取り出せということですね。何が困るかと言うとテストコードからのアクセスであればテストコードのどこかで起動してポート番号を取ってしまえばいいですが、やりたいことは何だったかと言うと、

  • テスト対象のSpring BootアプリケーションのRedisTemplateからアクセスする

でしたね。さてどうしよう。

マニュアルと読み進めるとSpring Bootでのサンプルなるものが公開されています。

testcontainers/testcontainers-java-examples
Archived: This repo has been combined into the main testcontainers-java repository - testcontainers/testcontainers-java-examples

ApplicationContextInitializerを実装してTestPropertyValuesを設定すると書いてあります。そんなことできるんだ。。。勉強になりますね。

とはいえ、テストの基底クラスとか個人的には好きじゃないのでApplicationContextInitializerはこんな感じにしてみました。

class SampleInitializer : ApplicationContextInitializer {

    val log: Logger = LoggerFactory.getLogger(SampleInitializer::class.java)

    override fun initialize(configurableApplicationContext: ConfigurableApplicationContext) {
        val redisContainer: KGenericContainer = KGenericContainer("redis:latest").withExposedPorts(6379)
        redisContainer.start()

        val containerIpAddress = redisContainer.getContainerIpAddress()
        val mappedPort = redisContainer.getMappedPort(6379)

        log.info("spring.redis.host=${containerIpAddress}")
        log.info("spring.redis.port=${mappedPort}")
        val values = TestPropertyValues.of(
                "spring.redis.host=${containerIpAddress}",
                "spring.redis.port=${mappedPort}"
        )
        values.applyTo(configurableApplicationContext)
        configurableApplicationContext.beanFactory.registerSingleton("redisContainer", redisContainer)
    }
}
class KGenericContainer(imageName: String) : GenericContainer(imageName)

Initializerのなかでコンテナ起動してコンテナのインスタンスもついでにBean登録。テストコード内でコンテナ停止したくなることもあるかもしれないし。。。でも1回止めるとポート番号が変わっちゃうというジレンマ。その辺はうまいことやってください。

ちなみにKGenericContainerってなんだ?という話ですが、kotlinだと素直に書けないんですって。

Problems with TestContainers and Kotlin · Issue #318 · testcontainers/testcontainers-java
TestContainers depends on construction of raw types and pattern like class C<SELF extends C<SELF>>. Unfortunately Kotlin, and probably other JVM lan[記事を読む]

このissueにかかれていたワークアラウンドを使っています。

んで、実際のテストコードからはこうやって使います。

@ExtendWith(SpringExtension::class)
@SpringBootTest
@ContextConfiguration(initializers = [SampleInitializer::class])
class TestcontainersSampleApplicationTests {

    @Autowired
    lateinit var redisContainer: KGenericContainer;

    @Autowired
    lateinit var redisTemplate: StringRedisTemplate;

    @Test
    fun redisContainerTest() {
        assertThat(redisContainer).isNotNull
    }

    @Test
    fun redisTest01() {
        redisTemplate.opsForValue().set("key01", "value01")
        assertThat(redisTemplate.opsForValue().get("key01")).isEqualTo("value01");
    }

    @Test
    fun redisTest02() {
        redisTemplate.opsForValue().set("key02", "value02")
        assertThat(redisTemplate.opsForValue().get("key02")).isEqualTo("value02");
    }
}

@Autowiredで取ってきたStringRedisTemplateでうまいことアクセスできていますね。application.ymlには特に何も書かずにデフォルトでコンフィグされたものを使っています。

テストケースによってはうまくはまらないケースももしかしたらあるかもしれませんが、まぁ大体これでなんとかなるんじゃないでしょうか。コンテナの起動もRedisなら数秒でできるので速度面でもまぁまぁ実用に堪えるんじゃないでしょうか。

以前にembedded-redisなんていうライブラリを使っていたこともあるのですが、いまいち使いづらかったですしアップデート頻度もいまいち。

kstyrc/embedded-redis
Redis embedded server for Java integration testing - kstyrc/embedded-redis

かと言ってMockにしてしまうのも勿体ない気がするのでこれからはtestcontainersおすすめですね。

データベース系は要注意

データベース系はGenericContainerでイメージを指定して起動するのではなくて、専用のライブラリがデータベース製品ごとに用意されています。MySQL,Postgres,Oracleといったような。

専用のJDBCドライバが作り込まれていたりして、なかなかリッチです。コンテナの起動を実装しなくてもConnectionを張るときにコンテナを起動する、なんて動きをしたりしてなかなか面白いですよ。その他細々とクセが強いですが使いこなせるとなかなか便利です。

ただ、データベース系のコンテナは起動が重いです。1分とか2分とかかかるのでそれが許容できるかどうかも採用可否のポイントになるかと思います。ただ、インメモリDBでは対応できないテスト(トリガとか使っていたり)もあるかと思うので、許容できるなら使うのがいいかな〜と思ったりもします。

使い捨て環境の嬉しさ

ビルドの冪等性と並列性を以下に担保するかってなかなか難しい話で、今でもテスト環境が予約できなくて泣きそうです(´;ω;`)なんてお嘆きのエンジニアの皆様たくさんいると思うんですけれど。私もいろんな現場で環境の枯渇を目撃してきましたが。。。

なるべくテストコードを書いて、かつそれが冪等かつ平行に動作する使い捨て環境が用意できれば、待たされることなくガンガン開発できるし、ちゃんとテストコードがあるってことは品質も担保しやすいってことですもんね。テストコードのメンテナンスに関してはまー・・・色々あるとは思いますが無いよりゃあったほうがいいと思う派です。

ビルド環境もクラウドの時代。

札束でひっぱたけ(゚Д゚)!!

まとめ

Testcontainers便利です。データベース系は重くて難しいですが、Redisとかだったら軽いし難しくもないのでかなりおすすめ。もちろんRedis以外も色々できるのでお試しください。

仕事で知ったライブラリなので自分のブログ記事にするのもなんかなぁ〜と思っていたのですが、まぁ会社で使ってない使い方&環境で自分で試した内容ならまぁいいか、ということで記事にしてみました(仕事では主にMySQL/Oracleで使っています)。

説明したサンプルはこの辺においてあります。

kawakamitor/blog-materials
Contribute to kawakamitor/blog-materials development by creating an account on GitHub.

ご興味があれば是非。

Spring&Dockerなら

この辺の本がおすすめ。

この記事に対するコメント