【サーバーサイドKotlin】KotlinTestによるKotlin × SpringBootの単体テスト

こんにちは、サーバーサイドエンジニアの竹端です。

以前【サーバーサイドKotlin】ことりん × テスト with SpringFrameworkにてKotlinのテストフレームワークの検証内容をご紹介をしました。

この時はSpek、KotelinTestというKotlin製のフレームワークがSpring Frameworkとの相性の問題で使えず、JUnitを採用するという検証結果になっていました。

しかし、その後KotlinTestにアップデートがあり、Spring FrameworkのDIに対応がされたため、そちらへ移行することとなりました。

今回はKotlinTestをSpring Frameworkを併せて使った際のテストコードの書き方についてご紹介します。

今回使用する環境

  • Kotlin 1.2.51
  • KotlinTest 3.1.8
  • SpringBoot 2.0.4.RELEASE

事前準備

テスト対象のクラスの用意

今回テストに関する内容のご紹介ということで、先にテストを実行する対象のクラスを作っておきます。

SampleService.kt
class SampleService {
    fun execute(param: Int): String {
        if (param == 1) {
            return "one"
        }
        if (param == 2) {
            return "two"
        }
        return "default"
    }
}

パラメータに1を渡したらone、2を渡したらtwo、それ以外ならdefaultという文字列を返すだけの関数を持ったクラスです。

テストの紹介が目的なので、こちらはシンプルなものになっています。

Gradleに依存関係の追加

Gradleのdependenciesに、下記を追加します。

testCompile("io.kotlintest:kotlintest-runner-junit5:3.1.8")
testCompile("io.kotlintest:kotlintest-extensions-spring:3.1.8")

Springを使用しない場合は、1行目の設定のみで大丈夫です。

KotlinTestとは?

KotlinTestはKotlin製のテストフレームワークで、多機能であることが特徴です。

StringSpec, FunSpec, ShouldSpec, WordSpec, FeatureSpec, BehaviorSpec、AnnotationSpec等、様々な書き方がサポートされています。

まずは、この中の2つの書き方をご紹介します。

書き方の例の紹介

BehaviorSpec

以前の記事で検証に使っていた記述方法です。

Behaviorという言葉の通り、振る舞い駆動開発をするのにも適した書き方になっています。

SampleServiceForBehaviorSpecTest.kt
class SampleServiceForBehaviorSpecTest : BehaviorSpec() {
    init {
        val service = SampleService()

        given("executeで") {
            `when`("paramが1の場合") {
                val result = service.execute(1)

                then("oneが返る") {
                    result shouldBe "one"
                }
            }

            `when`("paramが2の場合") {
                val result = service.execute(2)

                then("twoが返る") {
                    result shouldBe "two"
                }
            }
        }
    }
}

BehaviorSpec という抽象クラスを継承することで、BehaviorSpecの書き方をすることができます。

他の〜Specという書き方も、同じように継承するクラスを変えることで、その書き方をできるようになります。

また、実行結果の検証処理にもKotlinTestの機能を使っています。

このサンプルコードでは shouldBe をというキーワードを使うことで、値の等価性を検証しています。

BehaviorSpecに関しては、 given when then という3つのブロックで構成され、分かりやすいコードになることと、実装者による差分が生まれづらいことを魅力に感じ、こちらを試していました。

実行結果も次のように、階層構造で表示されます。

4c48ee2c-1310-7758-1302-ed53501712c7

StringSpec

現在アプリボットで採用している記述方法がこちらになります。

最もシンプルな記述方法で、下記のようなコードになります。

SampleServiceForStringSpecTest.kt
class SampleServiceForStringSpecTest : StringSpec() {
    init {
        val service = SampleService()

        "executeでparamが1の場合oneが返る" {
            service.execute(1) shouldBe "one"
        }

        "executeでparamが2の場合twoが返る" {
            service.execute(2) shouldBe "two"
        }
    }
}

文字列でテストケースの名前を定義し、その中で検証処理を書く非常にシンプルな形式になります。

実行結果は次のようになります。

ddc2c357-1ca1-472a-a21d-e21f0768bf0e

StringSpecを採用した理由

StringSpecはKotlinTestの開発者も推奨しており、公式ドキュメントでの情報も充実しています。

また、KotlinTestには特定のテストのみの実行、除外をするためにFocusBangという機能が用意されているのですが、こちらがsingle top level testのみをサポートしているため、BehaviorSpec等では正常に動かない場合がありました。

このことも、StringSpecの採用に傾いた要因の一つになりました。

実装例は下記になります。

Focus

名前の先頭に f: を付けたテストケースのみ実行されます。

val service = SampleService()

"executeでparamが1の場合oneが返る" {
    service.execute(1) shouldBe "one"
}

"f:executeでparamが2の場合twoが返る" {
    service.execute(2) shouldBe "two"
}

実行結果

215ed813-1bb2-86d7-77bd-cd079c6aa483

Bang

名前の先頭に ! を付けたテストケースを除外して実行されます。

val service = SampleService()

"executeでparamが1の場合oneが返る" {
    service.execute(1) shouldBe "one"
}

"!executeでparamが2の場合twoが返る" {
    service.execute(2) shouldBe "two"
}

実行結果

8907d577-d5b8-a2f0-a618-46d6d8e7955b.png

forallを使ったデータ駆動テスト

KotlinTestの強力な機能の一つとして、forallによるデータ駆動テストがあります。

下記のようなテストの書き方ができます。

init {
    val service = SampleService()

    forall(
            row(1, "one"),
            row(2, "two")
    ) { param, result ->
        "executeでparamが${param}の場合${result}が返る" {
            service.execute(param) shouldBe result
        }
    }
}

row に書いてある値をパラメータをparam、resultという名前で受け取り、それぞれのパラメータで下のテストコードを実行します。

このコードでは、先程のexecuteという関数に1、2の2つの値を渡して実行し、それぞれ併せて設定しているone、twoという値が返ってきているかをテストしています。

こうすることによって、先程のStringSpecのテストコードを一つにまとめることができました。

このように、異なるパラメータの組み合わせで実行していくテストのことを、データ駆動テストと言います。

実行結果も先程と同じように表示されます。

3e37242e-ceba-9e84-17fb-ebfecdb76879.png

また、下記のような関数のテストをしようとした場合も便利です。

fun execute2(param: Int): Boolean {
    if (param >= 10 && param <= 100) {
        return true
    } else {
        return false
    }
}

通常であれば境界値のテストとして、

  • paramが10の場合true
  • paramが100の場合true
  • paramが9の場合false
  • paramが101の場合false

という4パターンのテストコードを書く必要があります。

しかし、KotlinTestのforallを使用すれば

forall(
    row(10, "最小値"),
    row(100, "最大値")
) { num, description ->
    "execute2でparamが${description}の場合trueが返る" {
    val service = SampleService()
    service.execute2(num) shouldBe true
    }
}

forall(
    row(9, "最小値-1"),
    row(101, "最大値+1")
) { num, desctiption ->
    "execute2でparamが${desctiption}の場合falseが返る" {
    val service = SampleService()
    service.execute2(num) shouldBe false
    }
}

このように境界値の上下のテストを一つにして書けるので、余分なテストデータやテストコードを削減することができます。

他にもランダムな値を渡して繰り返し実行してくれる機能など、非常に魅力的な機能もあるので、詳しくは公式ドキュメントを読んでいただければと思います。

Spring Frameworkを使った場合のテスト

さて、ここまででKotlinTestについては説明してきましたが、いよいよSpring Frameworkと合わせたテストについて書いていこうかと思います。

※SpringBootを使用するためのGradleの設定は今回は省略しますので、Spring Initializr等を使用して環境に併せて作成していただければと思います。

まず、最初に記載したテスト対象のクラスをSpringの推奨の形に併せてインターフェース、実装クラスに分けます。

SpringSampleService.kt
interface SpringSampleService {
    fun execute(param: Int): String
}
SpringSampleServiceImpl.kt
@Component
class SpringSampleServiceImpl : SpringSampleService {
    override fun execute(param: Int): String {
        if (param == 1) {
            return "one"
        }
        if (param == 2) {
            return "two"
        }
        return "default"
    }
}

実装クラスの方にはDIの対象とするための @Componentを忘れずに付けてください。

次に、Springの設定クラスを作ります。

TestApplicationContext.kt
@ComponentScan
class TestApplicationContext

今回はComponentScanで全パッケージを対象とする設定だけをアノテーションでしています。

次がテストクラスです。

SpringSampleServiceImplTest.kt
@ContextConfiguration(classes = [TestApplicationContext::class])
class SpringSampleServiceImplTest(
        private val service: SpringSampleService
) : StringSpec() {
    init {
        "executeでparamが1の場合oneが返る" {
            service.execute(1) shouldBe "one"
        }

        "executeでparamが2の場合twoが返る" {
            service.execute(2) shouldBe "two"
        }
    }
}

@ContextConfiguration のアノテーションでパラメータに先程作った設定クラスを渡します。

また、今回はSpringで最も一般的なConstructor Injectionで SpringSampleService のDIをしています。

そして、次がKotlinTestでSpringを扱う際の重要なポイントとなります。

AbstractProjectConfig というクラスを継承し、下記のクラスを実装します。

ProjectConfig.kt
package io.kotlintest.provided

import io.kotlintest.AbstractProjectConfig
import io.kotlintest.extensions.ProjectLevelExtension
import io.kotlintest.spring.SpringAutowireConstructorExtension

class ProjectConfig : AbstractProjectConfig() {
    override fun extensions(): List<ProjectLevelExtension> = listOf(SpringAutowireConstructorExtension)
}

ここでオーバーライドしている extensions メソッドで SpringAutowireConstructorExtension というクラスを設定して返すことで、SpringのConstructor Injectionがテストで使えるようになります。

この時注意が必要で、パッケージは io.kotlintest.provided に配置し、クラス名は ProjectConfig という名前を必ず固定で付けなくてはいけません

そのため、このサンプルコードのみパッケージ定義含めた全行を記載しています。

この extensions メソッドで返しているListに追加して返すことで、様々な機能が使えるようになります。

また、AbstractProjectConfigの中には listeners filters という関数も用意されており、併せて設定が必要になる場合があります。

今回はConstructor Injectionを使う設定のみを追加しているので、フィールドインジェクションを使う場合も別の設定が必要になります。

そちらも詳しくは公式ドキュメントをご覧いただければと思います。

最後に

今回はKotlinTestのご紹介と、Spring Frameworkで使用する際の方法をご紹介しました。

Spring対応が入ってからまだ間もないこともあり、日本語での情報も少ないので、こちらの記事がこれから使う方のお役に少しでも立てばと思います。

また、アプリボットではここから更にプロダクトで扱いやすいよう共通化し、テスト基盤を作っています。

そちらも、いずれこのブログでご紹介できればと思います。