Kotest統合(Integrations)

Kotestとモッキングフレームワークmockk、JaCoCoを一緒に使うことで、より堅牢で品質の高いテストを書ける。ここでは、テストの分離と依存関係管理を助けるmockkと、コードベースのどれだけの部分がテストされたかを測定できるコードカバレッジツールJaCoCoについて説明する。

MockingとKotest

Kotest自体にはモックテスト機能がない。しかし、好みのモックライブラリを簡単にプラグインできる。

たとえば、mockkを見てみよう。

class MyTest : FunSpec({

    val repository = mockk<MyRepository>()
    val target = MyService(repository)

    test("Saves to repository") {
        every { repository.save(any()) } just Runs
        target.save(MyDataClass("a"))
        verify(exactly = 1) { repository.save(MyDataClass("a")) }
    }
})

この例は期待どおりに動作するが、同じmockkを使うテストをさらに追加するとどうなるだろうか。

class MyTest : FunSpec({

    val repository = mockk<MyRepository>()
    val target = MyService(repository)

    test("Saves to repository") {
        every { repository.save(any()) } just Runs
        target.save(MyDataClass("a"))
        verify(exactly = 1) { repository.save(MyDataClass("a")) }
    }

    test("Saves to repository as well") {
        every { repository.save(any()) } just Runs
        target.save(MyDataClass("a"))
        verify(exactly = 1) { repository.save(MyDataClass("a")) }
    }

})

上のスニペットは例外を発生させる。

2 matching calls found, but needs at least 1 and at most 1 calls

これは、呼び出しの間にモックがリセットされないために発生する。デフォルトでは、Kotestは実行するすべてのテストに対してSpecの単一インスタンスを生成してテストを分離する。

そのため、モックが再利用される。この問題はどのように解決できるだろうか。

オプション1 - テスト前にモックを設定する

class MyTest : FunSpec({

    lateinit var repository: MyRepository
    lateinit var target: MyService

    beforeTest {
        repository = mockk()
        target = MyService(repository)
    }

    test("Saves to repository") {
        // ...
    }

    test("Saves to repository as well") {
        // ...
    }

})

オプション2 - テスト後にモックをリセットする

class MyTest : FunSpec({

    val repository = mockk<MyRepository>()
    val target = MyService(repository)

    afterTest {
        clearMocks(repository)
    }

    test("Saves to repository") {
        // ...
    }

    test("Saves to repository as well") {
        // ...
    }

})

リスナーの位置を指定する

Spec定義内で実行されるすべての関数は、末尾にリスナーを配置できる。

class MyTest : FunSpec({

    val repository = mockk<MyRepository>()
    val target = MyService(repository)

    test("Saves to repository") {
        // ...
    }

    test("Saves to repository as well") {
        // ...
    }

    afterTest {
        clearMocks(repository)  // <---- End of file, better readability
    }

})

オプション3 - 分離モードを調整する

用途によっては、特定Specの分離モードを調整するのもよい方法である。分離モードについて詳しく知りたい場合は、分離モード文書を参照すること。

class MyTest : FunSpec({

    val repository = mockk<MyRepository>()
    val target = MyService(repository)


    test("Saves to repository") {
        // ...
    }

    test("Saves to repository as well") {
        // ...
    }

    isolationMode = IsolationMode.InstancePerTest

})

JaCoCo

Kotestは、標準的なGradle方式でコードカバレッジのためにJaCoCoと統合される。Gradleのインストール手順はここで確認できる。

  1. Gradleでpluginsにjacocoを追加する。
    plugins {
        ...
        jacoco
        ...
    }
    
  2. jacocoを構成する。
    jacoco {
        toolVersion = "0.8.11"
        reportsDirectory = layout.buildDirectory.dir('customJacocoReportDir') // optional
    }
    
  3. jacoco XMLレポートタスクを追加する。
    tasks.jacocoTestReport {
        dependsOn(tasks.test)
        reports {
            xml.required.set(true)
        }
    }
    
  4. testタスクがjacocoに続くよう変更する。
    tasks.test {
        ...
        finalizedBy(tasks.jacocoTestReport)
    }
    

これでtestを実行すると、JaCoCoレポートファイルが$buildDir/reports/jacocoに生成される。

Note: マルチモジュールプロジェクトの場合、各サブモジュールにjacocoプラグインを適用する必要がある場合がある。


参照