Kotestの基本的な書き方

Kotestの基本的な書き方、テスト実行、レポートについて説明する。

テストコードの書き方

Kotestのインストールと設定が完了したら、テストコードの書き方について見ていく。

Kotlinが提供する言語機能を使うと、テストを定義するための、より強力でシンプルなアプローチを利用できる。テストをJavaファイルのメソッドとして定義しなければならなかった時代は過ぎた。

Kotestでは、テストは本質的にテストロジックを含むTestContext -> Unit関数にすぎない。この関数内で呼び出されたすべてのアサーション文(Kotest用語ではMatchers)は、例外を発生させるとフレームワークに捕捉され、そのテストを失敗または成功として示すために使用される。

テスト関数は手動で定義するのではなく、これらの関数を生成し、ネストできる複数の方法を提供するKotest DSLを使って定義する。DSLは、特定のテストスタイルを実装するクラスを拡張したクラスを作成して記述する。

任意のパスにテストクラスを作成した後、テストスタイルであるSpecクラスを継承する必要がある。

例えば、Fun Specスタイルを使用するには、FunSpecクラスを継承し、testキーワード、名前、実際のテスト関数を使ってテスト関数を作成する。

class MyFirstTestClass : FunSpec({

   test("my first test") {
      1 + 2 shouldBe 3
   }

})

テストコードの内部を書く方法には、主にDSL(Domain-Specific Language)で書く方法と、init {}ブロックを使う方法の2つがある。

この2つの方法について説明する。

DSLで書く

まず、DSL(Domain-Specific Language)で書く方法について説明する。

Kotestはテストを書くためのDSLを提供する。このDSLを使うと、テストをより自然に表現できる。
各テストスタイルに対応するDSLが提供されており、それを使ってテストを構成できる。例えば、StringSpecWordSpecFunSpecなどは、それぞれ文字列、単語、関数を使ってテストを定義するDSLを提供する。

これらのDSLはテストを構造化して表現するうえで便利であり、可読性と保守性を向上させる。

import io.kotest.core.spec.style.StringSpec

class MyStringSpec : StringSpec({
    "This is a test" {
        // テストコードを作成する
    }
})

initブロックを指定して書く

Kotlinのinitブロックを使って、テストを初期化し設定できる。これは一般的に、より多くの柔軟性が必要な場合に使用される。

initブロックはテストクラスのコンストラクタ内部に位置し、クラスがインスタンス化されるときに初期化される。そのため、テストクラスのすべてのメソッドでこれを共有できる。

import io.kotest.core.spec.style.StringSpec

class MyStringSpec : StringSpec() {
    init {
        "This is a test" {
            // テストコードを作成する
        }
    }
}

DSLとinitブロックは、それぞれ異なるユースケースや開発者の好みに応じて選択される。より構造化されたテストを書きたい場合はDSLを使用でき、特定の設定が必要な場合はinitブロックを使用できる。

ネストしたテスト

ほとんどのスタイルは、テストをネストする機能を提供する。実際の構文はスタイルごとに異なるが、基本的には外側のテストに使用されるキーワードが異なるだけである。

例えば、Describe Specでは外側のテストをdescribe関数で作成し、内側のテストをit関数で作成する。JavaScriptやRubyの開発者は、それぞれの言語のテストフレームワークで一般的に使われているため、このスタイルをすぐに理解できる。

class NestedTestExamples : DescribeSpec({

   describe("an outer test") {

      it("an inner test") {
        1 + 2 shouldBe 3
      }

      it("an inner test too!") {
        3 + 4 shouldBe 7
      }
   }

})

Kotest用語では、他のテストを含められるテストをテストコンテナ(test containers)と呼び、終端またはリーフノードのテストをテストケース(test cases)と呼ぶ。どちらもテストロジックとアサーションを含めることができる。

動的テスト

テストは単なる関数なので、実行時に検証される。

このアプローチには、テストを動的に生成できるという大きな利点がある。テストが常にメソッドでありコンパイル時に宣言される従来のJVMテストフレームワークとは異なり、Kotestは実行時に条件付きでテストを追加できる。

例えば、リストの要素に基づいてテストを追加できる。

class DynamicTests : FunSpec({

    listOf(
      "sam",
      "pam",
      "tim",
    ).forEach {
       test("$it should be a three letter name") {
           it.shouldHaveLength(3)
       }
    }
})

これにより、実行時に3つのテストが生成される。次のように書くのと同じである。

class DynamicTests : FunSpec({

   test("sam should be a three letter name") {
      "sam".shouldHaveLength(3)
   }

   test("pam should be a three letter name") {
      "pam".shouldHaveLength(3)
   }

   test("tim should be a three letter name") {
     "tim".shouldHaveLength(3)
   }
})

ライフサイクルコールバック

Kotestはテストライフサイクル中のさまざまな時点で呼び出される複数のコールバックを提供する。これらのコールバックは、状態のリセット、テストで使用するリソースの設定と解放などに役立つ。

前述のように、Kotestのテスト関数はテストコンテナまたはテストケースとしてラベル付けされ、包含クラスはSpecとしてラベル付けされる。テスト関数、コンテナ、テストケース、またはSpec自体の前後に呼び出されるコールバックを登録できる。

コールバックを登録するには、コールバックメソッドのいずれかに関数を渡すだけでよい。

例えば、関数リテラルを使ってテストケースの前後にコールバックを追加できる。

class Callbacks : FunSpec({

   beforeEach {
      println("Hello from $it")
   }

   test("sam should be a three letter name") {
      "sam".shouldHaveLength(3)
   }

   afterEach {
      println("Goodbye from $it")
   }
})

ファイル内でのコールバックの順序は重要ではない。例えば、必要ならafterEachブロックをクラスの最初に配置してもよい。

共通コードを抽出するには、名前付き関数を作成して複数のファイルで再利用できる。例えば、2つ以上のファイルですべてのテスト前にデータベースをリセットしたい場合、次のようにできる。

val resetDatabase: BeforeTest = {
  // truncate all tables here
}

class ReusableCallbacks : FunSpec({

   beforeTest(resetDatabase)

   test("this test will have a sparkling clean database!") {
       // test logic here
   }
})

すべてのコールバックの詳細と、コールバックが呼び出されるタイミングについては、Lifecycle HooksExtensionsを参照する。

AssertionとMatchers

KotestはさまざまなMatchersを提供し、テスト結果を検証できる。これにより、期待結果と実際の結果を比較し、テストが成功したかを確認できる。例えば、上の例でresult shouldBe 2の部分は、resultが2と等しいかを検証するAssertionである。

そのほかにも多くのMatchersが存在する。以下はKotestを使って主なMatchersを使用するサンプルコードである。

package com.devkuma.kotest.tutorial.assertions

import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldContainAll
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.collections.shouldNotContain
import io.kotest.matchers.ints.shouldBeGreaterThan
import io.kotest.matchers.ints.shouldBeInRange
import io.kotest.matchers.ints.shouldBeLessThan
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldBeBlank
import io.kotest.matchers.string.shouldBeEqualIgnoringCase

class MatchersTest : FreeSpec({

    "Matchers" - {
        val number = 10
        "値が期待値と同じかを確認する" {
            number shouldBe 10
        }

        "値が期待値と異なるかを確認する" {
            number shouldNotBe 20
        }

        val blankString = ""
        "文字列が空白だけで構成されているかを確認する" {
            blankString.shouldBeBlank()
        }

        val numbers = listOf(1, 2, 3, 4, 5)

        "コレクションが指定した要素を含むかを確認する" {
            numbers shouldContain 3
        }

        "コレクションが指定した要素を含まないかを確認する" {
            numbers shouldNotContain 6
        }

        "コレクションが指定したすべての要素を含むかを確認する" {
            numbers shouldContainAll listOf(2, 4)
        }

        "コレクションが指定した要素と完全に一致するかを確認する" {
            numbers.shouldContainExactly(1, 2, 3, 4, 5)
        }

        val value = 10
        "値が指定した範囲に含まれるかを確認する" {
            value shouldBeInRange 5..15
        }

        "値が指定した値より大きいかを確認する" {
            value shouldBeGreaterThan 5
        }

        "値が指定した値より小さいかを確認する" {
            value shouldBeLessThan 15
        }

        val message = "Hello, Kotest"
        "大文字小文字を無視して一致するかを確認する" {
            message shouldBeEqualIgnoringCase "HELLO, KOTEST"
        }
    }
})

このほかにも多くのMatchersが存在し、詳細は改めて説明する。

テスト実行とレポート

テストを実行するには、IDEでテストを実行するか、GradleまたはMavenを使ってテストを実行できる。テスト実行後は、テスト結果のレポートを確認できる。Kotestはテスト結果を詳細に記録し、レポートを生成して、開発者がテストの状態を把握できるようにする。

テストを実行するには、IDEで該当するテストクラスやファイルを実行するか、GradleまたはMavenを使ってテストを実行できる。テスト実行後は、各テストケースの結果をレポート形式で確認できる。Kotestは実行されたテストの結果を詳細なレポートとして提供し、テストの成功または失敗を把握できるようにする。

IntelliJ IDEAでテストを実行する

IntelliJ IDEAでテストクラスおよびファイルを選択し、左側のKotestタブを選択すると、下のようにテストケースが表示される。
IntelliJ IDEA実行

表示されたテストケースを選択した状態でRunボタンを押すと、下のようにテストコードが実行されることを確認できる。
IntelliJ IDEA実行結果

Gradleテスト実行

KotestをGradle経由で実行する方法は次のとおりである。

テストコマンドの実行

ターミナルまたはコマンドウィンドウでプロジェクトディレクトリへ移動した後、次のコマンドを実行してGradleでプロジェクトのテストを実行する。

./gradlew test

実行が完了すると、テスト結果がターミナルに出力される。以下は、テストが通過した場合にBUILD SUCCESSFULメッセージが出力される例である。

% ./gradlew test

BUILD SUCCESSFUL in 548ms
3 actionable tasks: 3 up-to-date

失敗したテストが発生すると、BUILD FAILEDメッセージが出力される。

 % ./gradlew test 

BUILD SUCCESSFUL in 28s
3 actionable tasks: 3 executed
user@AL02263852 kotest-tutorial % ./gradlew test

> Task :test

com.devkuma.kotest.tutorial.MyTest > My first test FAILED
    io.kotest.assertions.AssertionFailedError at MyTest.kt:9

58 tests completed, 1 failed, 8 skipped

> Task :test FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/user/develop/kotlin-tutorial/kotest-tutorial/build/reports/tests/test/index.html

* Try:
> Run with --scan to get full insights.

BUILD FAILED in 26s
3 actionable tasks: 2 executed, 1 up-to-date

テストレポートの確認

テスト実行後、build/reports/testsディレクトリに生成されたHTML形式のテストレポートを確認すると、テスト結果を詳しく見られる。

上で失敗した場合に出力された内容を見ると、テストレポートのリンクが次のように表示されている。

> There were failing tests. See the report at: file:///Users/user/develop/kotlin-tutorial/kotest-tutorial/build/reports/tests/test/index.html

このファイルをブラウザで開くと、下のように表示される。
Test Summary失敗

実行テストが何件か、失敗した件数が何件かが詳細に表示される。

これは成功した場合でも、コマンド出力に表示されないだけで、同じファイル位置にファイルが生成される。

Test Summary成功


このように、Kotestを使用すると、簡単な設定とテストコード作成で、容易かつ効率的にテストを実行できる。次のセクションでは、Kotestのさらに多くの機能と活用方法について扱う。

参考