Kotestはいいぞ - 基本構文編

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

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

Kotest基本構文編 -> 今回
[mockk編](https://qiita.com/maiya0123/items/d0fbbdfa0e56525da812) -> 次回

# そもそも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をより効率的に使用するためのサポートツールについても紹介していきます。