Kotestカスタムmatcher(Custom Matchers)
カスタムmatcher
Kotestでは自分だけのmatcherを簡単に定義できる。
Matcher<T>インターフェースを拡張するだけでよい。ここでTは一致させたい型である。Matcherインターフェースは、MatcherResultのインスタンスを返す1つの関数testを定義する。
interface Matcher<in T> {
fun test(value: T): MatcherResult
}
このMatcherResult型は、テスト通過 여부を表すブール値を返す関数と2つの失敗メッセージ、計3つの関数を定義する。
interface MatcherResult {
fun passed(): Boolean
fun failureMessage(): String
fun negatedFailureMessage(): String
}
- passed
- アサーションを満たすか(true)、満たさないか(false)を表す。
- failureMessage
- 一致条件が失敗した場合にユーザーへ送るメッセージである。
- アサーションが失敗したときに表示し、成功させるには何をすべきかを知らせるメッセージである。
- 一般に期待値と実際値の詳細や差分を含めることができる。
- negatedFailureMessage
- 一致条件が否定モードで真と評価された場合にユーザーへ送るメッセージである。
- Matcherを否定で使用し、Matcherが失敗する場合に表示すべきメッセージである。
- ここには一般に、述語が失敗すると期待していたことを示す。
カスタムmatcher定義
2つの失敗メッセージの違いは、例を通じてより明確に分かる。文字列に必要な長さがあるか確認するために、文字列向けの長さmatcherを書くと仮定しよう。構文はstr.shouldHaveLength(8)のような形式が必要である。
すると最初のメッセージは"string had length 15 but we expected length 8"のようになるべきである。2つ目のメッセージは"string should not have length 8"のようになるべきである。
まずmatcher型を実装する。
import io.kotest.matchers.Matcher
import io.kotest.matchers.MatcherResult
fun haveLength(length: Int) = Matcher<String> { value ->
MatcherResult(
value.length == length,
{ "string had length ${value.length} but we expected length $length" },
{ "string should not have length $length" },
)
}
エラーメッセージを関数呼び出しでラップし、必要でない場合は評価しないようにする。これは生成に時間がかかるエラーメッセージの場合に重要である。
その後、このmatcherを次のようにshouldおよびshouldNot接頭辞関数へ渡すことができる。
"hello foo" should haveLength(9)
"hello bar" shouldNot haveLength(3)
拡張バリアント
ここではmatcher関数を呼び出し、関数チェーンのために元の値を返す拡張関数を定義する。これはKotestが組み込みmatcherを構成する方法であり、KotestはshouldXYZ命名戦略に合わせようとする。例:
fun String.shouldHaveLength(length: Int): String {
this should haveLength(length)
return this
}
fun String.shouldNotHaveLength(length: Int): String {
this shouldNot haveLength(length)
return this
}
これにより、次のように呼び出せる。
"hello foo".shouldHaveLength(9)
"hello bar".shouldNotHaveLength(3)
偶数を含むか確認するカスタムmatcher定義
次に、リストに偶数が含まれているか確認するカスタムMatcherを作ってみよう。
次はcontainEvenNumbersというカスタムMatcherを作り、リストに2で割って余りが0になる要素があるかを確認している。
package com.devkuma.kotest.tutorial.assertions.custommatchers
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.*
import io.kotest.matchers.should
fun containEvenNumbers() = Matcher<List<Int>> { value ->
MatcherResult(
value.any { it % 2 == 0 },
{ "List should contain even numbers" },
{ "List should not contain even numbers" }
)
}
fun List<Int>.shouldContainEvenNumbers(): List<Int> {
this should containEvenNumbers()
return this
}
infix fun <T> T.should(matcher: (T) -> Unit) = matcher(this)
class ContainEvenNumbersCustomMatcher : FunSpec({
test("should") {
val list = listOf(1, 2, 3, 4, 5)
list should containEvenNumbers()
}
test("shouldContainEvenNumbers") {
val list = listOf(1, 2, 3, 4, 5)
list.shouldContainEvenNumbers()
}
})
カスタムmatcherによってテストコードの可読性を高められる。ただし、命名を誤るとかえって逆効果になる可能性がある点に注意しよう。