Kotestを導入してみた
目次
はじめに
参画中のプロジェクトでテストフレームワークのKotestを導入しました。
実際にUnitTestを書いての所感をあれこれ書いてみたので、どなたかの参考になれば。
Kotestについて
Kotlinファーストなテストフレームワーク。
- Test Framework
- Assertions Library
- Property Testing
の3つで構成されてて、それぞれ独立して導入可能です。
Kotest is a flexible and elegant multi-platform test framework for Kotlin with extensive assertions and integrated property testing
とのこと。フレキシブルでエレガント。
詳しくは公式ドキュメントを参照してください。
導入
build.gradleにこれらを追加(JUnit4系との共存を気にしないならrunnerだけでOK)して
testImplementation 'io.kotest:kotest-runner-junit5:$version'
testImplementation 'org.junit.vintage:junit-vintage-engine:$version'
こうします
testOptions {
unitTests {
all {
it.useJUnitPlatform()
}
}
}
簡単ですね。
JUnitと見比べてみる
JUnit(4系)のテストコードはこんな感じ↓
class SomeTest{
@Test
fun someFunctionのテスト_正常系(){
val input = "入力値"
val expected = "期待値"
Assert.assertEquals(expected, someFunction(input))
}
@Test
fun someFunctionのテスト_異常系(){
val input = "入力値"
val expected = "期待値"
Assert.assertNotEquals(expected, someFunction(input))
}
@Test
fun anotherFunctionのテスト(){
val input = "入力値"
val expected = "期待値"
Assert.assertEquals(expected, anotherFunction(input))
}
}
Kotestではこのように書きます↓
class SomeTest:FunSpec({
context("someFunctionのテスト"){
test("正常系"){
val input = "入力値"
val expected = "期待値"
someFunction(input) shouldBe expected
}
test("異常系"){
val input = "入力値"
val expected = "期待値"
someFunction(input) shouldNotBe expected
}
}
test("anotherFunctionのテスト"){
val input = "入力値"
val expected = "期待値"
anotherFunction(input) shouldBe expected
}
})
書き方
書き方 – Spec
Spec = テストケースの書き方のスタイル、みたいなイメージですかね。
FunSpecやDescribeSpecなど10種類が用意されていて、出来ることに差はないらしいです。
ScalaTestやJUnitなど、元ネタというかインスパイア元があったりするようなので使い慣れてるものを選びましょう、ということでしょうか。
Some teams prefer to mandate usage of a single style, others mix and match. There is no right or wrong – do whatever feels right for your team.
とのこと。
class SomeTests : FunSpec({
context("正常系") {
test("テストケース") {
XXX
}
test("テストケース"){
XXX
}
}
context("異常系") {
context("条件Aの異常系"){
test("テストケース") {
XXX
}
test("テストケース") {
XXX
}
}
context("条件Bの異常系"){
test("テストケース"){
XXX
}
}
}
})
initブロックについて
initブロックを使ってテストを記述することも出来ます。
FunSpec({})
とするかFunSpec(){}
とするかの違いで、後者で書くならinit{}
が必要になる感じですね。
基本的には好みで良さそう(個人的にはネスト減らせるのでinit無しの方が嬉しい)ですが、beforeTest
をオーバーライドするときなんかはこっちの書き方でないとダメとのこと。
class MyFirstTestClass : FunSpec(){
override fun beforeTest(f: suspend (TestCase) -> Unit) {
super.beforeTest(f)
doSomething()
}
init {
// tests here
}
}
書き方 – アサーション
基本形は以下です。
someFunction(input).shouldBe(expected)
shouldBe
はinfixな関数なので下記のようにも書けます。someFunction(input) shouldBe expected
ですが、shouldBeだけでもほとんど事足りそうな雰囲気…?
Clueについて
clue = 手がかり、糸口。
テスト失敗したときのためのヒントを追加できる機能で、コード上にコマゴマとコメント残すよりシンプルに書けそうです。
withClueでAssertionErrorに好きな文言が出力でき、
withClue("$inputは$expectedになるはずなのに……"){
someFunction(input) shouldBe expected
}
また以下のようにobject全体をClueとして出力することも可能です。
letやalsoの代わりにとりあえずasClueにしておく、という使い方も良いかも。
object.asClue{
it.status shouldBe 200
it.message shouldBe "Success"
}
データ駆動テスト
kotest-framework-datatest
を追加すればデータ駆動テストっぽい書き方もできます。
パターン多くてもゴチャっとならなくて良いですね。
class MyTest : FunSpec({
context("データ駆動テスト") {
/**
* テストパターンのdata class
* @param input 入力値
* @param expected 期待値
*/
data class TestPattern(
val input: String,
val expected: String
)
withData(
mapOf( // mapにすることでテストケースに命名できる
"テストケース1" to TestPattern("入力値1", "期待値1"),
"テストケース2" to TestPattern("入力値2", "期待値2"),
"テストケース3" to TestPattern("入力値3", "期待値3")
)
) { (input, expected) ->
someFunction(input) shouldBe expected
}
}
})
所感
良いところ
- JUnit4と比べて記法がシンプル、かつ単純に書く量も少なくて済む。
- contextの組み合わせでテストケースを階層化でき、テスト観点が網羅出来ているか視覚的に分かりやすい。
- DataTestがとにかく便利。
- coroutineのテストのサポート機能なども用意されているらしく、ViewModelのテストなどがやりやすくなりそう。
いまいちなところ
- 記法の自由度が高いので、チームやプロジェクトごとにある程度のルールは定めておかないとレビューや読解のコストが嵩んでしまうかも。
- 膨大なアサーションを使いこなすのは中々大変そう