💨

Kotestでのユニットテスト - Kotest基本構文編

2022/10/30に公開

はじめに

Kotlinが開発言語として使用されている開発環境の場合、テストにJUnitが使われることが多いと思います。しかし、Kotestというテスティングフレームワークを使えば、JUnitに比べKotlinのSyntaxを使えるので、コード量を減らせることができます。また、テストケースをネストして書くことができる利点があります。

そこで今回はKotestの基本構文についてまとめてみようと思います。加えて、次回はこのKotestをサポートするライブラリをについても記載していきます。

Kotestでのユニットテスト - Kotest基本構文編 -> 今回
Kotestでのユニットテスト - mockk編 -> 次回
Kotestでのユニットテスト - testcontainers編

そもそもKotestとは

Kotlinで使えるテスティングフレームワークです。以前はKotlintestという名前でしたがkotestという名前に変わったようです。まずサンプルコードを見てみましょう。
※以下公式のサンプルテストコード

class MyTests : StringSpec({
   "length should return size of string" {
      "hello".length shouldBe 5
   }
   "startsWith should test for a prefix" {
      "world" should startWith("wor")
   }
})

Specについて

上記のサンプルコードの中にSpecという文言があります。これは簡単に言えば、テストの種類です。どんなテストを書きたいかでこのSpecを使い分けます。

String Spec

もっともシンプルな構文。テストケースを文字列で記載できて、ブロック単位でテストする。
ただし階層は1階層での記述しかできない。

class CalcServiceStringSpecTest : StringSpec() {
    private val calc = Calc()
    
    init {
        "1 + 1 は2になる" {
            calc.plus(1, 1) shouldBe 2
        }
        "5 ÷ 0 は例外が投げられる" {
            shouldThrow<ArithmeticException> { calc.divide(5, 0) }
        }
    }
}

Fun Spec

testという関数を実行して、処理を呼び出します。contextを使って階層構造にすることも可能です。
これ以降は基本的に階層でテストを表していきます。

class CalcServiceFunSpecTest : FunSpec() {
    private val calc = Calc()

    init {
        context("CalcServiceTest") {
            context("正常系") {
                test("1 + 1 は2になる") {
                    calc.plus(1, 1) shouldBe 2
                }
            }
            context("異常系") {
                test("5 ÷ 0 は例外が投げられる") {
                    shouldThrow<ArithmeticException> { calc.divide(5, 0) }
                }
            }
        }
    }
}

Expect Spec

Fun Specのtestのかわりにexpectという関数を実行して処理を呼び出します。

class CalcServiceExpectSpecTest : ExpectSpec() {
    private val calc = Calc()

    init {
        context("CalcServiceTest") {
            context("正常系") {
                expect("1 + 1 は2になる") {
                    calc.plus(1, 1) shouldBe 2
                }
            }
            context("異常系") {
                expect("5 ÷ 0 は例外が投げられる") {
                    shouldThrow<ArithmeticException> { calc.divide(5, 0) }
                }
            }
        }
    }
}

Should Spec

Fun Specのtestのかわりにshouldという関数を実行して処理を呼び出します。
文字列にネストすることもできます。

class CalcServiceShouldSpecTest : ShouldSpec() {
    private val calc = Calc()

    init {
        context("CalcServiceTest") {
            context("正常系") {
                should("1 + 1 は2になる") {
                    calc.plus(1, 1) shouldBe 2
                }
            }
            context("異常系") {
                should("5 ÷ 0 は例外が投げられる") {
                    shouldThrow<ArithmeticException> { calc.divide(5, 0) }
                }
            }
	    // 文字列ネストも可能
            "正常系の場合" {
                should("1 + 1 は2になる") {
                    calc.plus(1, 1) shouldBe 2
                }
            }
            "異常系の場合" {
                should("5 ÷ 0 は例外が投げられる") {
                    shouldThrow<ArithmeticException> { calc.divide(5, 0) }
                }
            }
        }
    }
}

Describe Spec

describe、context、testのかわりitでテストを定義して処理を呼び出します。

class CalcServiceDescribeSpecTest : DescribeSpec() {
    private val calc = Calc()

    init {
        describe("CalcServiceTest") {
            context("正常系") {
                it("1 + 1 は2になる") {
                    calc.plus(1, 1) shouldBe 2
                }
            }
            context("異常系") {
                it("5 ÷ 0 は例外が投げられる") {
                    shouldThrow<ArithmeticException> { calc.divide(5, 0) }
                }
            }
        }
    }
}

Behavior Spec

given、when、thenでテストを定義して処理を呼び出します。

class CalcServiceBehaviorSpecTest : BehaviorSpec() {
    private val calc = Calc()

    init {
        Given("CalcServiceTest") {
            when("正常系") {
                Then("1 + 1 は2になる") {
                    calc.plus(1, 1) shouldBe 2
                }
            }
            when("異常系") {
                Then("5 ÷ 0 は例外が投げられる") {
                    shouldThrow<ArithmeticException> { calc.divide(5, 0) }
                }
            }
        }
    }
}

Free Spec

文字列テキストと - を定義して処理を呼び出します。階層化できるので書きやすいですが、どういった種類のテストか判別する際に分かりにくいというデメリットもあります。

class CalcServiceBehaviorSpecTest : BehaviorSpec() {
    private val calc = Calc()

    init {
        "CalcServiceTest" - {
            "正常系" - {
                "10 割る 5 は 2 になる" - {
                    calc.plus(1, 1) shouldBe 2
                }
            }
            "異常系" - {
                "5 ÷ 0 は例外が投げられる" - {
                    shouldThrow<ArithmeticException> { calc.divide(5, 0) }
                }
            }
        }
    }
}

アサーションについて

テストコードの中で判定する部分となるアサーションについても主に代表的な2つを挙げておきます。

shouldBe / shouldNotBe

値がそうなっているべきなshouldBe、値がそうなっていないべきなshouldNotBe

class CalcServiceStringSpecTest : StringSpec() {
    private val calc = Calc()

    init {
        "1 + 1 は2になる" {
            calc.plus(1, 1) shouldBe 2
        }
        "1 + 1 は3にならない" {
            calc.plus(1, 1) shouldNotBe 3
        }
    }
}

shouldThrow

例外をテストする場合に使うshouldThrow

class CalcServiceStringSpecTest : StringSpec() {
    private val calc = Calc()
    
    init {
        "5 ÷ 0 は例外が投げられる" {
            shouldThrow<ArithmeticException> { calc.divide(5, 0) }
        }
    }
}

まとめ

Specを書く(ShouldSpecが使い勝手が良さそう)→context文で、テストのcontextを決める→その中で処理を書いてアサーションで判定していくコードを書いていく、が基本的な書き方かと思います。次回はKotestをより効率的に使用するためのサポートツールについても紹介していきます。

Discussion