Spek2でのテストの書き方

DroidKaigi2019でもSpekに関するセッションがあり、触発されたのでとりあえず使い方をざっくりまとめます。

- DroidKaigi2019でのセッション
    - [Spek2+MockK+JaCoCoでイケてるUnit Test環境を手に入れろ！](https://droidkaigi.jp/2019/timetable/70535/)
    - [SpekでUnitTestを書こう](https://droidkaigi.jp/2019/timetable/69464)

以下、Spekのバージョンは2.0.0の前提で記載します。

# Spekとは

[公式ドキュメント](https://spekframework.org/)

平たく言うと、Kotlinで書かれたRspec風テストフレームワークです。
スタイルは以下の２種類を提供しています。

-  [specification](https://spekframework.org/specification/)
-  [gherkin](https://spekframework.org/gherkin/)

## SetUp

[Setting Up - Android](https://spekframework.org/setup-android/)

## 動作要件

- JDK8
- JUnit5

## 注意点

- Android integration テストには対応していません
- RobolectricなどはJUnit4で書く必要があるため、両方で動かすには`junit-vintage`なども入れる必要があります

# 書き方

ここでは  [specification](https://spekframework.org/specification/) スタイルでの解説になります。

上記ドキュメントの「[Best practices](https://spekframework.org/specification/#best-practices)」のセクションに記載されていますが、以下２つの観点でテストを実装すると良いです。

- 副作用を検証する
- 戻り値を検証する

また、specification でのコンセプトは以下の通りです

## Suites

- `describe`, `context`を用いて仕様のグループ化に利用します。
- 入れ子で定義することもできます。
- `context`は`describe`の中で定義することができます。
- 通常、以下のように記載することが多いようです。
    - `describe`
        - テスト対象
    - `context`
        - テスト条件

## Specs

- `it`を用いて何をチェクするかなどのアウトプットに関する内容を記述します。

## Skipping

- `describe`, `context`, `it`に`x`をつけることで、そのテストをスキップすることができます。
- もしくは、第二引数にSkip.Yesを指定することでスキップすることもできます。
- テストにスキップの指定をする場合は必ず理由を記述します。

## Aliases

- `describe`, `context`ブロックのテスト前後に実行したい処理は`before` / `after` や `beforeEach` / `afterEach`を利用して記載します。
- `before` / `after` は`beforeGroup` / `afterGroup` と同じで、各`describe`, `context`ブロックの開始 / 終了時に呼ばれます
- `beforeEach` / `afterEach` は `beforeEachTest` / `afterEachTest` と同じで宣言したブロック内（入れ子も含む）のテストの前後で呼ばれます
- 入れ子のブロックでそれぞれ宣言した場合の呼び出し順序は以下の通りです
    - beforeは外側で宣言したものからafterは内側で宣言したものから呼ばれます
    - 入れ子になっていても宣言されている`before`がすべて呼ばれた後に`beforeEach`が呼ばれます（`after`はその逆）

以下、入れ子での呼び出し順を試したサンプルコードとその実行結果です。

```kotlin
@RunWith(JUnitPlatform::class)
class SpekSampleTest : Spek({
    describe("1 - describe") {
        before {
            println("1 - before")
        }
        beforeEach {
            println("1 - beforeEach")
        }
        context("1 - 1 - context") {
            before {
                println("1 - 1 - before")
            }
            beforeEach {
                println("1 - 1 - beforeEach")
            }
            it("1 - 1 - it") {
                println("1 - 1 - it")
            }
            afterEach {
                println("1 - 1 - afterEach")
            }
            after {
                println("1 - 1 - after")
            }
        }
        context("1 - 2 - context") {
            it("1 - 2 - it") {
                println("1 - 2 - it")
            }
        }
        afterEach {
            println("1 - afterEach")
        }
        after {
            println("1 - after")
        }
    }
    describe("2 - describe") {
        before {
            println("2 - before")
        }
        beforeEach {
            println("2 - beforeEach")
        }
        context("2 - context") {
            it("2 - 1 - it") {
                println("2 - 1 - it")
            }
            it("2 - 2 - it") {
                println("2 - 2 - it")
            }
        }
        afterEach {
            println("2 - afterEach")
        }
        after {
            println("2 - after")
        }
    }
})
```

```
1 - before
1 - 1 - before
1 - beforeEach
1 - 1 - beforeEach
1 - 1 - it
1 - 1 - afterEach
1 - afterEach
1 - 1 - after
1 - beforeEach
1 - 2 - it
1 - afterEach
1 - after
2 - before
2 - beforeEach
2 - 1 - it
2 - afterEach
2 - beforeEach
2 - 2 - it
2 - afterEach
2 - after
```

# 記述例

```kotlin
@RunWith(JUnitPlatform::class)
class SpekSampleTest : Spek({
    val set = mutableListOf<String>()

    describe("adding an item") {
        beforeEachTest {
            set.add("item")
        }

        it("should contain item") {
            assertEquals("item", set[0])
        }

        it("should have a size > 0") {
            assertTrue(set.size > 0)
        }
    }
})
```

# 最後に

あとは実際に書く時に、`describe`や`context`などにどんな記述で書くか、テストをスキップさせる場合はどうするかなどをチーム内でルール化しておくと良いと思います。