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のインストール手順はここで確認できる。
- Gradleでpluginsにjacocoを追加する。
plugins { ... jacoco ... } - jacocoを構成する。
jacoco { toolVersion = "0.8.11" reportsDirectory = layout.buildDirectory.dir('customJacocoReportDir') // optional } - jacoco XMLレポートタスクを追加する。
tasks.jacocoTestReport { dependsOn(tasks.test) reports { xml.required.set(true) } } - testタスクがjacocoに続くよう変更する。
tasks.test { ... finalizedBy(tasks.jacocoTestReport) }
これでtestを実行すると、JaCoCoレポートファイルが$buildDir/reports/jacocoに生成される。
Note: マルチモジュールプロジェクトの場合、各サブモジュールにjacocoプラグインを適用する必要がある場合がある。