Kotest Custom Matchers

In Kotest, you can create custom matchers to check specific conditions.

Custom Matchers

You can easily define your own matchers in Kotest.

You only need to extend the Matcher<T> interface. Here, T is the type you want to match. The Matcher interface defines one function, test, which returns an instance of MatcherResult.

interface Matcher<in T> {
   fun test(value: T): MatcherResult
}

The MatcherResult type defines three functions: a function that returns a boolean indicating whether the test passed, and two failure messages.

interface MatcherResult {
   fun passed(): Boolean
   fun failureMessage(): String
   fun negatedFailureMessage(): String
}
  • passed
    • Indicates whether the assertion is satisfied, true, or not satisfied, false.
  • failureMessage
    • The message shown to the user when the match condition fails.
    • It is shown when an assertion fails and tells what needs to happen for it to succeed.
    • It can generally include details and differences between expected and actual values.
  • negatedFailureMessage
    • The message shown to the user when the match condition evaluates to true in negated mode.
    • This is the message that should be displayed when a matcher fails while using negation.
    • It generally indicates that the predicate was expected to fail.

Defining a Custom Matcher

The difference between the two failure messages becomes clearer with an example. Suppose you are writing a length matcher for strings to check whether a string has the required length. The desired syntax is something like str.shouldHaveLength(8).

Then the first message should be like "string had length 15 but we expected length 8". The second message should be like "string should not have length 8".

First, implement the matcher type:

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" },
    )
}

The error messages are wrapped in function calls so they are not evaluated unless needed. This is important for error messages that take time to generate.

Then you can pass this matcher to the should and shouldNot prefix functions as follows:

"hello foo" should haveLength(9)
"hello bar" shouldNot haveLength(3)

Extension Variants

Here, we define an extension function that calls the matcher function and returns the original value for function chaining. This is how Kotest structures its built-in matchers, and Kotest tries to follow the shouldXYZ naming strategy. For example:

fun String.shouldHaveLength(length: Int): String {
    this should haveLength(length)
    return this
}

fun String.shouldNotHaveLength(length: Int): String {
    this shouldNot haveLength(length)
    return this
}

This lets you call it as follows:

"hello foo".shouldHaveLength(9)
"hello bar".shouldNotHaveLength(3)

Defining a Custom Matcher that Checks for Even Numbers

Next, let’s create a custom matcher that checks whether a list contains even numbers.

The following creates a custom matcher named containEvenNumbers and checks whether the list contains a number that leaves a remainder of 0 when divided by 2.

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()
    }
})

Custom matchers can improve the readability of test code. However, keep in mind that poor naming can have the opposite effect.

References