Spek2でのテストの書き方


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

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

Spekとは

公式ドキュメント

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

SetUp

Setting Up - Android

動作要件

  • JDK8
  • JUnit5

注意点

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

書き方

ここでは specification スタイルでの解説になります。

上記ドキュメントの「Best practices」のセクションに記載されていますが、以下2つの観点でテストを実装すると良いです。

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

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

Suites

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

Specs

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

Skipping

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

Aliases

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

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

@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

記述例

@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)
        }
    }
})

最後に

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