タケハタのブログ

プログラマの生き方、働き方、技術について雑多に書いていくブログです。

KotlinTestの色々なSpecの書き方を試してみた

f:id:take7010:20180930201632p:plain

はじめに

この前会社の技術ブログでKotlinTestに関する記事を書きました。
blog.applibot.co.jp

この記事ではKotlinTestの導入とSpringBootと併せて使う方法についてご紹介しました。
そこでは会社で2種類のSpecの書き方を紹介していたのですが、その他のSpecの書き方についても個人的に試してみたので、その解説を書きたいと思います。

各Specの解説

公式ドキュメントとしては下記のものがあるので、こちらを参考に、順番に沿って解説していきます。
github.com

導入方法については冒頭でご紹介した会社の技術ブログの記事をご確認ください。
また、その記事の中で使用している下記のSampleServiceクラスを使用して、各Specのサンプルコードを書いていきます。

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

StringSpec

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"
        }
    }
}

一番シンプルな構文です。
テストケースの名前を文字列で記述し、そのブロックにテストケースを記述していく形になります。
テストケースの階層も単一になります。

FunSpec

class SampleServiceForFunSpecTest : FunSpec() {
    init {
        val service = SampleService()

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

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

test という関数にテスト名を文字列で渡して呼び出し、テストの処理をクロージャとして呼び出しています。

ShouldSpec

class SampleServiceForSholdSpecTest : ShouldSpec() {
    init {
        val service = SampleService()

        should("executeでparamが1の場合oneを返す") {
            service.execute(1) shouldBe "one"
        }
        should("executeでparamが2の場合twoを返す") {
            service.execute(2) shouldBe "two"
        }
    }
}

FunSpec とほぼ同等ですが、test の代わりに should というキーワードを使用しています。

FunSpec との機能的な違いとしては、StringSpecのようなコンテキスト文字列にネストすることができます。

class SampleServiceForSholdSpecTest : ShouldSpec() {
    init {
        val service = SampleService()

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

            }
            "paramが2の場合" {
                should("twoを返す") {
                    service.execute(2) shouldBe "two"
                }

            }
        }
    }
}

ネストが使えるので、後述する BehaviorSpec みたいな書き方も擬似的にできそう。

WordSpec

class SampleServiceForWordSpecTest: WordSpec() {
    init {
        "executeで" should {
            val service = SampleService()

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

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

文字列の後ろに should を付けて、ネストした中でStringSpecと同じように文字列でテストケースを定義します。
ShouldSpec をのネストの外側と内側を逆にした感じ。
ただし、 ShoudSpec は下記のように内側の文字列で多重にネストするとエラーになります。

"executeで" should {
    val service = SampleService()

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

FeatureSpec

class SampleServiceForFeatureSpecTest: FeatureSpec() {
    init {
        feature("executeで") {
            val service = SampleService()
            
            scenario("paramが1の場合oneを返す") {
                service.execute(1) shouldBe "one"
            }
            
            scenario("paramが2の場合twoを返す") {
                service.execute(2) shouldBe "two"
            }
        }
    }
}

featurescenario というキーワードを使い、ネストして記述します。
scenario の中にさらに scenario をネストすることはできないので、feature で分類して、 scenario で関連するテストケースを羅列していく感じですかね。

cucumberというテストフレームワークでシナリオを書く時の形に似ているらしいです(使ったことないのであまり分かってないです)。

BehaviorSpec

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

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

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

名前の通り、BDDに則した書き方で、 given when then の3つのブロックで構成します。
分かりやすい書き方なんですが、 when がKotlinの予約語として存在するので、バッククォートで括らないといけないのが若干気持ち悪いですね。

FreeSpec

class SampleServiceForFreeSpecTest : FreeSpec() {
    init {
        "executeで" - {
            val service = SampleService()

            "paramが1の場合" - {
                val result = service.execute(1)
                "oneが返る" {
                    result shouldBe "one"
                }
            }

            "paramが2の場合" - {
                val result = service.execute(2)
                "twoが返る" {
                    result shouldBe "two"
                }
            }
        }
    }
}

文字列の後ろに - を付けることで、ネストしてテストケースを書くことができます。
ネストは制限なく、任意の深度まで書けます。

DescribeSpec

class SampleServiceForDescribeSpecTest : DescribeSpec() {
    init {
        val service = SampleService()

        describe("executeで") {
            context("paramが1の場合") {
                val result = service.execute(1)

                it("oneが返ってくる") {
                    result.shouldBe("one")
                }
            }

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

                it("twoが返ってくる") {
                    result.shouldBe("two")
                }
            }
        }
    }
}

BehaviorSpec に似ていますが、こちらは describe context it というキーワードで構成します。
RubyのRSpecというテストフレームワークを模倣しているらしいです(こちらも使ったことないのでよく分からない)。

ExpectSpec

class SampleServiceForExpectSpecTest : ExpectSpec() {
    init {
        context("executeで") {
            val service = SampleService()

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

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

FeatureSpec に似た書き方ですが、こちらは contextexpect というキーワードを使っています。
こちらも expect はネストできないため、 context で分類して、 expect で関連するテストケースを羅列していく感じですかね。

AnnotationSpec

class SampleServiceForAnnotationSpecTest : AnnotationSpec() {
    val service = SampleService()

    @Test
    fun paramが1の場合oneを返す() {
        service.execute(1) shouldBe "one"
    }

    @Test
    fun paramが2の場合twoを返す() {
        service.execute(2) shouldBe "two"
    }
}

JavaでおなじみJUnitライクな書き方です。
init を書く必要がなく、 @Test のアノテーションを付けることでテストケースを定義します。
比較的最近追加された書き方になります。

データ駆動テストの対応

KotlinTestの強力な機能として、 forall をはじめとする機能を使った、データ駆動テストの機能があります。

こちらは一応全Specで対応しているとドキュメントには載っていますが、 BehaviorSpec ではまだ上手く動かせてないです・・・
他のSpecも全部は試してないので、こちらはまた別途色々試してみて書きますね。

Focus、Bangの対応

前述の会社の技術ブログでも書いていますが、FocusBangという機能があり、Focusがsingle top level testのみをサポートしています(Bangの方は使えそう)。

例えば FreeSpec でネストした際に、内側の oneが返る のブロックに使おうとしても、

class SampleServiceForFreeSpecTest : FreeSpec() {
    init {
        "executeで" - {
            val service = SampleService()

            "paramが1の場合" - {
                val result = service.execute(1)
                "f:oneが返る" {
                    result shouldBe "one"
                }
            }

            "paramが2の場合" - {
                val result = service.execute(2)
                "twoが返る" {
                    result shouldBe "two"
                }
            }
        }
    }
}

では効かず全部のテストが実行されてしまいます。
ネストをやめて下記のように書けば、 f: を付けた1個目のテストケースだけ実行されます(もはや StringSpec と変わらないですが)。

class SampleServiceForFreeSpecTest : FreeSpec() {
    init {
        val service = SampleService()

        "f:executeでparamが1の場合oneが返る" - {
            val result = service.execute(1)
            result shouldBe "one"
        }

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

こちらも他のSpecももうちょっと色々試してみて別途書きますね。

結局どのSpecが良いか?

今のところ一番安全そうなのは StringSpec
シンプル故に今回書いている forall Focus Bang をはじめとしたKotlinTestのいろんな機能が問題なく使えそうだし、開発者もこれを推奨してそうな感じ。

ただ、テストケースを階層化して書けるのは分かりやすいし魅力的なので本当は BehaviorSpec とか DescribeSpec を使いたい気持ちはあります。
それ以外は正直好みかなぁという感覚。
AnnotationSpec に至ってはこれ使うならJUnit使った方が良いのではと思う。

ただ、KotlinTestはアップデートも多いので、今後また変わっていくかもしれないです。
今回調べきれなかった内容も含め、また色々使ってみて書こうと思います。
(新しい機能出ると毎度10種類分試さないといけないので大変そうですがw)