Kotest 사용자 정의 매처(Custom Matchers)

Kotest에서는 커스텀 매처를 만들어 특정 조건을 확인할 수 있다.

사용자 정의 매처

Kotest에서 자신만의 매처를 쉽게 정의할 수 있다.

Matcher<T> 인터페이스를 확장하기만 하면 된다. 여기서 T는 일치시키고자 하는 유형이다. Matcher 인터페이스는 MatcherResult의 인스턴스를 반환하는 하나의 함수로 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가 실패하는 경우 표시해야 하는 메시지이다.
    • 여기에는 일반적으로 술어가 실패할 것으로 예상했음을 나타낸다.

사용자 정의 매처 정의

실패 메시지의 2개의 차이점은 예제를 통해 더 명확하게 알 수 있다. 문자열에 필요한 길이가 있는지 확인하기 위해 문자열에 대한 길이 매처를 작성한다고 가정해 보겠다. 구문은 str.shouldHaveLength(8)와 같은 형식이 필요하다.

그러면 첫 번째 메시지는 "string had length 15 but we expected length 8"와 같아야 한다. 두 번째 메시지는 "string should not have length 8"와 같아야 한다.

먼저 매처 유형을 구현한다:

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

오류 메시지를 함수 호출로 래핑하여 필요하지 않은 경우 평가하지 않도록 한다. 이는 생성하는 데 시간이 걸리는 오류 메시지의 경우 중요하다.

그런 다음 이 매처를 다음과 같이 shouldshouldNot 접두사 함수에 전달할 수 있다:

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

확장 변형

여기서는 matcher 함수를 호출하고, 함수 연결을 위한 원래 값을 반환하는 확장 함수를 정의하려고 한다. 이는 Kotest가 내장된 매처를 구성하는 방식이며, 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를 만들어 보자고 한다.

다음은 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()
    }
})

커스텀 매처를 통해 테스트 코드의 가독성을 높일 수 있다. 단, 네이밍을 잘못 하면 오히려 반대의 효과가 발생할 수 있다느 점을 유의하도록 하자.

참조




최종 수정 : 2024-04-14