Kotestの各種Specを比べてみた

### はじめに
Kotlinのプロジェクトで[Kotest](https://github.com/kotest/kotest)を使ってテストを書いてみた時に
Specって何？とか、結局何を使えばいいの？ で迷ったので調べたことをメモしておく。

### Kotestとは
Kotlinで使えるテストフレームワーク。
Javaに対するJUnitのような存在。
以前は`Kotlintest`という名前だったがRelease 4.0から`Kotest`に改名された。

後発だけあって、他の色々なテストフレームワークから良い所を取り込んでいたり
テストを色々な書き方（これが`Spec`）ができるのが特徴。

```kotlin:サンプルテストコード（公式より）
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』でOK
  + ネストが必要なら個人的には『Free Spec』がおススメ


### 環境
- Kotlin：1.3.61
- Kotest：4.0.6
- JUnit5：5.6.2
- Intelli J：2019.3（Community Edition）

### Specの一覧

| Spec            | 特徴                   |
| --------------- | -------------------- |
| String Spec     | 一番基本的なSpec           |
| Fun Spec        | ScalaTestと同じ書き方      |
| Expect SPec     | Fun Specに似ている        |
| Should Spec     | Fun Specに似ている        |
| Describe Spec   | RSpecと同じ書き方          |
| Behavior Spec   | BDD frameworksと同じ書き方 |
| Word Spec       | ScalaTestと同じ書き方      |
| Feature Spec    | Cucumberと同じ書き方       |
| Free Spec       | ScalaTestと同じ書き方      |
| Annotation Spec | JUnitと同じ書き方          |


### サンプル

#### テスト対象クラス
割り算するだけのクラス。

```kotlin
class Calc {
    fun divide(a: Int, b:Int): Int {
        return a / b
    }
}
```

#### JUnit5で書くとこんな感じ

```kotlin
class CalcTest_JUnit5 {
    private val calc = Calc()

    @Test
    fun テスト_10割る2は5() {
        assert(calc.divide(10, 2) == 5)
    }

    @Test
    fun テスト_10を0で割ると例外() {
        assertThrows<ArithmeticException> { calc.divide(10, 0) }
    }
}
```
JUnitはメソッド名を数字始まりにできないんだった。

実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15083/037df24b-38ba-dd6b-739a-b2e0ba1290f6.png)


階層化してみる。
JUnit5で簡単になったとは言え、かなり冗長な感じがする。

```kotlin
class CalcTest_JUnit5 {
    private val calc = Calc()

    @Nested
    inner class Calcのテスト {
        
        @Nested 
        inner class 正常系{
            @Test
            fun テスト_10割る2は5() {
                assert(calc.divide(10, 2) == 5)
            }
        }

        @Nested
        inner class 異常系{
            @Test
            fun テスト_10を0で割ると例外() {
                assertThrows<ArithmeticException> { calc.divide(10, 0) }
            }
        }
    }
}
```

実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15083/b6c622c1-dbd9-e9dd-b5ff-b18f2ae03b5c.png)



#### String Spec
公式でもおススメっぽく全ての機能が動く＆ネットでの情報も豊富。
（他のSpecは未サポート機能が割とある）

が、非常に残念ながら階層化できない。
人によっては割と致命的かもしれない?

```kotlin
class StringSpecのテスト : StringSpec() {
    private val calc = Calc()

    init {
        "10 割る 5 は 2 になる" {
            calc.divide(10, 5) shouldBe 2
        }

        "10 割る 0 は例外(ArithmeticException)が起きる" {
            shouldThrow<ArithmeticException> { calc.divide(10, 0) }
        }
    }
}
```

実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15083/fa38562f-f144-3bf4-593f-798b82cc8dce.png)


#### Fun Spec
`test` というキーワードでテストを定義する。

```kotlin
class FunSpecのテスト : FunSpec() {
    private val calc = Calc()

    init {
        test("10 割る 5 は 2 になる") {
            calc.divide(10, 5) shouldBe 2
        }
        
        test("10 割る 0 は例外(ArithmeticException)が起きる") {
            shouldThrow<ArithmeticException> { calc.divide(10, 0) }
        }
    }
}
```

`context` を使って階層化することもできる。

```kotlin
class FunSpecのテストをネストする : FunSpec() {
    private val calc = Calc()

    init {
        context("Calcのテスト") {
            context("正常系") {
                test("10 割る 5 は 2 になる") {
                    calc.divide(10, 5) shouldBe 2
                }
            }
            context("異常系") {
                test("10 割る 0 は例外(ArithmeticException)が起きる") {
                    shouldThrow<ArithmeticException> { calc.divide(10, 0) }
                }
            }
        }
    }
}
```

実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15083/7aa6c8ff-59b3-d2b0-df85-ddeb06674b42.png)


以後は階層化したものだけ記載する。

#### Expect Spec
Fun Specと似ている。

`test`の代わりに`expect`というキーワードでテストを定義する。
`context` を使って階層化することもできる。

```kotlin
class ExpectSpecのテスト : ExpectSpec() {
    private val calc = Calc()
    
    init {
        context("Calcのテスト") {
            context("正常系") {
                expect("10 割る 5 は 2 になる") {
                    calc.divide(10, 5) shouldBe 2
                }
            }
            context("異常系") {
                expect("10 割る 0 は例外(ArithmeticException)が起きる") {
                    shouldThrow<ArithmeticException> { calc.divide(10, 0) }
                }
            }
        }    }
}

```

実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15083/e04d5b16-d911-491c-4651-683456942324.png)


公式では
>Tests can be disabled using the xcontext and xexpect variants

と、`X-Methods`を使って無効化できると書いてあるが実際には動作しなかった。
公式のサンプルも間違っているので、このExpectSpecでは`X-Methods`は使えないっぽい。

```java
class MyTests : DescribeSpec({  // ★ 例がExpectSpecじゃなくてDescribeSpecになっている
    context("this outer block is enabled") {
        xexpect("this test is disabled") {
            // test here
        }
    }
```
#### Should Spec
Fun Specと似ている。

`test`の代わりに`should`というキーワードでテストを定義する。
`context` を使って階層化することもできる。

```kotlin
class ShouldSpecのテスト : ShouldSpec() {
    private val calc = Calc()

    init {
        context("Calcのテスト") {
            context("正常系") {
                should("10 割る 5 は 2 になる") {
                    calc.divide(10, 5) shouldBe 2
                }

            }
            context("異常系") {
                should("10 割る 0 は例外(ArithmeticException)が起きる") {
                    shouldThrow<ArithmeticException> { calc.divide(10, 0) }
                }
            }
        }
    }
}
```

実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15083/31949d64-c9e9-0e80-1059-f88f6b373553.png)


以前は階層化する際に`context`を使わず直接文字列で階層を定義できたが
非推奨になったっぽい。（一応動くけど、IDEAに怒られる）

```kotlin
class ShouldSpecのテスト_非推奨な書き方 : ShouldSpec() {
    private val calc = Calc()

    init {
        "Calcのテスト" {
            "正常系" {
                should("10 割る 5 は 2 になる") {
                    calc.divide(10, 5) shouldBe 2
                }

            }
            "異常系" {
                should("10 割る 0 は例外(ArithmeticException)が起きる") {
                    shouldThrow<ArithmeticException> { calc.divide(10, 0) }
                }
            }
        }
    }
}
```

#### Describe Spec
`describe`、`context`、`it` というキーワードでテストを定義する。
キーワードはそれぞれ複数回使うこともできる（逆に使わなくても良い）。

大昔にC++のプロジェクトで[igloo](https://github.com/joakimkarlsson/igloo)というフレームワークを使っていたのを思い出した。

```kotlin
class Describeのテスト() : DescribeSpec() {
    private val calc = Calc()
    
    init {
        describe("Calcのテスト") {
            context("正常系") {
                it("10 割る 5 は 2 になる") {
                    calc.divide(10, 5).shouldBe(2)
                }
            }
            context("異常系") {
                it("10 割る 0 は例外(ArithmeticException)が起きる") {
                    shouldThrow<ArithmeticException> { calc.divide(10, 0) }
                }
            }
        }
    }
}
```

実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15083/d553546b-cc62-72ac-cbd6-808acfd36ef1.png)


#### Behavior Spec
`given`、`when`、`then` というキーワードでテストを定義する。
BDDスタイルの書き方。

Kotlinでは`when`が予約語になっているため`` `when` ``みたいにバッククォートで括るか
もしくは先頭を大文字にして`When`と書く。
（他の予約語も先頭を大文字にできる）

```kotlin
class BehaviorSpecのテスト : BehaviorSpec() {
    private val calc = Calc()
    
    init {
        Given("Calcのテスト") {
            When("正常系") {
                Then("10 割る 5 は 2 になる") {
                    calc.divide(10, 5) shouldBe 2
                }
            }
            When("異常系") {
                Then("10 割る 0 は例外(ArithmeticException)が起きる") {
                    shouldThrow<ArithmeticException> { calc.divide(10, 0) }
                }
            }
        }
    }
}
```

実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15083/36664546-70b1-af46-1eab-fe2823cf306c.png)


`given`、`when`はそれぞれ複数回使ったり、更に`and`というキーワードもつかって階層化できるが
Intelli Jで実行するとテスト結果自体は階層化されていないため、こちらは可読性のためだけに使えるっぽい。

#### Word Spec
任意の文字列と`should` というキーワードでテストを定義する。
Word Specなのに`should`を使うのが紛らわしい。。。

`should`の外側に１階層だけ`when`を使って階層化できる。
また`should`の内側にも1階層だけ任意の文字列で階層化できる。

```kotlin
class WordSpecのテスト : WordSpec() {
    private val calc = Calc()
    
    init {
        "Calcのテスト" When {
            "正常系" should  {
                "10 割る 5 は 2 になる"  {
                    calc.divide(10, 5) shouldBe 2
                }
            }
            "異常系" should {
                "10 割る 0 は例外(ArithmeticException)が起きる"  {
                    assertThrows<ArithmeticException> { calc.divide(10, 0) }
                }
            }
        }
    }
}
```

実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15083/8f4eef3b-e73f-d0a5-a2db-a78843bbd907.png)


#### Feature Spec
`feature`、`scenario` というキーワードでテストを定義する。
cucumberスタイルの書き方。

`feature`はネストすることができる。

ExpectSPecと同様にX-Methodsによる無効化は動作しない。


```kotlin
class FeatureSpecのテスト : FeatureSpec() {
    private val calc = Calc()

    init {
        feature("Calcのテスト") {
            feature("正常系") {
                scenario("10 割る 5 は 2 になる") {
                    calc.divide(10, 5) shouldBe 2
                }
            }
            feature("異常系") {
                feature("10 割る 0 は例外(ArithmeticException)が起きる") {
                    assertThrows<ArithmeticException> { calc.divide(10, 0) }                    
                }
            }
        }
    }
}
```

実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15083/7648c539-2e20-d710-96fc-6de2cfc937f9.png)


#### Free Spec
任意の文字列と`-` というキーワードでテストを定義する。
文字列は任意の数だけ階層化できる。

『階層化できるString Spec』に近いのか？

キーワードを強制されず好きなだけ階層化できるのでシンプルに書けて自由度は高いが  
一方で無法地帯になって可読性が落ちる危険もある。

```kotlin
class FeeeSpecのテスト : FreeSpec() {
    private val calc = Calc()

    init {
        "Calcのテスト" - {
            "正常系" - {
                "10 割る 5 は 2 になる" - {
                    calc.divide(10, 5) shouldBe 2
                }
            }
            "異常系" - {
                "10 割る 0 は例外(ArithmeticException)が起きる" - {
                    assertThrows<ArithmeticException> { calc.divide(10, 0) }
                }
            }
        }
    }
}
```

実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15083/a33e024e-3459-23a3-2a25-3fda74bbd10b.png)


#### Annotation Spec
JUnitそのままの書き方。


公式にすら
>特にアドバンテージはない

と書かれている通り、JUnitの書き方に慣れた人がKotestで書きたい場合に使うことを意図しているらしいが
じゃそもそもなんでKotestを使うの？ という気が。。

@Nasted アノテーションによる階層化も普通にできた。
テストメソッドを`init{}`で括らなくて良いのが他のSpecと少しだけ違う。

```kotlin
class AnnotationSpecのテスト : AnnotationSpec() {
    private val calc = Calc()

    @Nested
    inner class Calcのテスト {

        @Nested
        inner class 正常系{
            @org.junit.jupiter.api.Test
            fun テスト_10割る2は5() {
                assert(calc.divide(10, 2) == 5)
            }
        }

        @Nested
        inner class 異常系{
            @org.junit.jupiter.api.Test
            fun テスト_10を0で割ると例外() {
                assertThrows<ArithmeticException> { calc.divide(10, 0) }
            }
        }
    }
}
```

しかしIntelli Jで実行するとなぜか一番外側のクラス単位にまとめられてしまった。
原因は不明。

実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15083/3cd8f4f7-90e8-9500-be9f-fc005f55929a.png)



### 最後に
他のテストフレームワーク（といってもGoogletestとJUnitを少しかじっただけど）と比べて、機能的に絶対的なメリット/デメリットはなくても、テストを簡潔に書けることや可読性にフォーカスしている感じがイマドキっぽく感じた。
Javaと混在のプロジェクトでは敢えてJUnitから乗り換えるほどのメリットはないと思うけど、ピュアなKotlinプロジェクトならこっちに寄せるのも面白そう。

ただし微妙に枯れていない感じもあって、公式に載っていてもちゃんと動かない機能があったり、ググっても情報が少なかったりするのでハマる危険は相対的に大きいと思う。
（まさにこれをやって[Exposed](https://github.com/JetBrains/Exposed)で絶賛ハマリ中。。そういえば[Javalin](https://javalin.io/)でもハマった。）

Kotestの機能的な話については、もう少し使い込んで情報がまとまったら別の記事にする。かも。


### 参考情報
- [Kotest](https://github.com/kotest/kotest)