Kotestカスタムmatcher(Custom Matchers)

Kotestではカスタムmatcherを作成して特定条件を確認できる。

カスタム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によってテストコードの可読性を高められる。ただし、命名を誤るとかえって逆効果になる可能性がある点に注意しよう。

参照