Skip to main content

Matchers personalizados

⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.


r8vnhill/testing-kt

Be lazy...

Puedes ejecutar el siguiente comando para crear el módulo

./gradlew setupCustomMatchersModule

Mientras se crean los archivos necesarios, puedes leer el código para saber qué está pasando.

import tasks.ModuleSetupTask

tasks.register<ModuleSetupTask>("setupCustomMatchersModule") {
description = "Creates the base module and files for the custom matchers lesson"
module.set("matchers:custom")
doLast {
createFiles(
"metrics",
main to "IdentifierGenerator.kt",
test to "IdentifierGeneratorTest.kt",
)
createFiles(
"metrics.matchers",
test to "beEven.kt",
test to "beCloseTo.kt",
)
}
}

Preocúpate de que el plugin customMatchers esté aplicado en el archivo build.gradle.kts de tu proyecto.

./gradlew setupCustomMatchersModule

Preocúpate de que el nuevo módulo esté incluido en el archivo settings.gradle.kts.

Cuando trabajamos con tests, los matchers nos permiten expresar expectativas de forma clara y legible: esperamos que una colección esté vacía, que un número sea mayor que otro, que un string contenga cierto texto, etc.

Pero no todos los casos están cubiertos por los matchers estándar. ¿Qué pasa si queremos verificar si un número es primo? ¿O si un objeto cumple una condición específica de nuestra lógica de dominio?

Ahí es donde entran los matchers personalizados: funciones reutilizables que nos permiten extender el sistema de aserciones de Kotest para expresar verificaciones propias de nuestra aplicación o librería.

🧪 Ejemplo de Matcher Personalizado

En Kotest, todos los matchers implementan la interfaz Matcher<T>, donde T representa el tipo de valor que se desea verificar. Para crear un matcher personalizado, basta con definir el método test, que retorna un MatcherResult con el resultado de la verificación y los mensajes apropiados en caso de éxito o error.

Imaginemos un caso real: estás desarrollando una biblioteca de métricas que genera identificadores para eventos de rendimiento. Por diseño, estos identificadores deben ser números pares (quizás por razones de alineación en memoria o consistencia del sistema). Para expresar esta regla de negocio de forma clara y reutilizable en tus pruebas, puedes definir un matcher llamado beEven().

fun beEven() = Matcher<Int> { value ->
MatcherResult(
passed = value % 2 == 0,
failureMessageFn = { "$value should be even" },
negatedFailureMessageFn = { "$value should not be even" }
)
}
¿Qué acabamos de hacer?

Este matcher define una regla de negocio simple pero importante: que todos los identificadores generados deben ser pares. La función beEven() devuelve un objeto Matcher<Int>, que verifica esta condición y entrega mensajes personalizados para los casos de éxito y de error.

Esto permite que las pruebas no solo validen el comportamiento de la biblioteca, sino que también comuniquen con claridad qué propiedad se espera del sistema. Al encapsular la lógica dentro de un matcher reutilizable, evitamos repetir código y logramos que las pruebas sean más expresivas y alineadas con el dominio del problema.

Ahora puedes utilizarlo en una prueba que verifique que todos los identificadores generados son pares:

withData(ids) { id ->
id should beEven()
}
Nota

Aunque aquí reimplementamos beEven() como ejemplo didáctico, Kotest ya incluye este matcher en su conjunto estándar. Puedes usarlo directamente incluyendo el módulo kotest-assertions-core en tu proyecto:

gradle/libs.versions.toml
[libraries]
kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest-framework" }

[bundles]
kotest = ["kotest-runner-junit5", "kotest-datatest", "kotest-assertions-core"]

📏 Ejercicio: Matcher de cercanía

Ejercicio

Supón que estás desarrollando una biblioteca de métricas o análisis numérico, y necesitas verificar si dos valores de punto flotante son suficientemente cercanos. Esto es común en cálculos donde intervienen aproximaciones, operaciones con decimales o resultados de funciones matemáticas que pueden acumular pequeños errores por precisión.

Implementa un matcher beCloseTo: (Double, Double) -> Matcher<Double> que permita comparar si un valor Double es suficientemente cercano a otro, dentro de una tolerancia especificada. Por ejemplo, 0.1 should beCloseTo(0.2, 0.01) debería ser falso, pero 0.1 should beCloseTo(0.11, 0.02) debería pasar.

Este matcher puede ser útil en bibliotecas que procesan medidas físicas, estadísticas o valores que provienen de simulaciones, donde rara vez se espera una igualdad exacta.

Ver hint
  • Puede serte de utilidad la función kotlin.math.abs: (Double) -> Double de Kotlin, que retorna el valor absoluto de un número.
Solución
fun beCloseTo(expected: Double, tolerance: Double) = Matcher<Double> { value ->
MatcherResult(
passed = abs(value - expected) < tolerance,
failureMessageFn = {
"$value should be in the vicinity of $expected within a tolerance of $tolerance"
},
negatedFailureMessageFn = {
"$value should not be in the vicinity of $expected within a tolerance of $tolerance"
}
)
}

🎯 Conclusiones

Los matchers personalizados son una herramienta clave para escribir pruebas expresivas y alineadas con el dominio de tu biblioteca. Permiten capturar reglas de negocio específicas, comunicar expectativas con claridad y reducir la repetición de código en los tests.

Al encapsular la lógica de verificación dentro de funciones reutilizables, también se facilita el mantenimiento y la evolución de la biblioteca, ya que las aserciones pueden mantenerse sincronizadas con el comportamiento esperado del sistema.

🔑 Puntos clave

  1. Extensibilidad natural
    Kotest permite crear nuevos matchers fácilmente, integrándolos sin fricción con el resto del framework.
  2. Legibilidad y mantenimiento
    Los matchers personalizados mejoran la claridad de las pruebas al expresar reglas de negocio en el lenguaje del dominio.
  3. Aplicabilidad real
    Son especialmente útiles en bibliotecas con reglas específicas o cálculos numéricos donde los matchers genéricos no son suficientes.

🧰 ¿Qué nos llevamos?

Al construir bibliotecas de software, no basta con que el código “funcione”: también debemos asegurar que su comportamiento sea confiable, claro y validable. Los matchers personalizados nos permiten transformar reglas de negocio en aserciones expresivas, haciendo que las pruebas no solo verifiquen comportamientos, sino que comuniquen intención.

A lo largo de esta sección vimos cómo extender Kotest con funciones reutilizables que elevan el nivel de abstracción en nuestras pruebas. Al hacerlo, nuestras bibliotecas se benefician de una base de tests más robusta, alineada con el dominio y fácil de mantener. En definitiva, aprendimos que probar no es solo verificar que algo no se rompa, sino también afirmar, con claridad, qué se espera que haga.

📖 Referencias

🔥 Recomendadas

🌐 Custom Matchers | Kotest. (s. f.). Recuperado 22 de marzo de 2025, de https://kotest.io/docs/assertions/custom-matchers.html