Ir al contenido principal
Versión: 6.2 🚧

Debería ser

Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

El principal matcher o aserción en Kotest es shouldBe. Este matcher se utiliza para verificar la igualdad entre un valor real y uno esperado. La sintaxis sigue el formato actual shouldBe expected. Por ejemplo:

val a = "samuel"
val b = a.take(3)
b shouldBe "sam"

Cuando dos valores no son iguales, Kotest mostrará un mensaje de error detallado que incluye un enlace de intellij <click to see difference> entre ambos valores. Por ejemplo:

Expected :world
Actual :hello
<Click to see difference>

Nota: puedes verificar que dos valores no son iguales usando shouldNotBe.

val a = "samuel"
val b = a.take(3)
b shouldNotBe "bob"
consejo

El matcher shouldBe puede combinarse con power assert para obtener mayor efectividad.

Internamente, Kotest utiliza el método equals pero añade lógica adicional para determinar igualdad en tipos donde la comparación simple de objetos no es adecuada. Por ejemplo, en JVM es conocido que Arrays con el mismo contenido no se considerarán iguales al usar el método equals. Otro ejemplo son primitivos de diferentes tipos aunque tengan el mismo valor.

Esta lógica está encapsulada en la typeclass Eq que Kotest usa internamente. También es posible definir tu propia lógica de igualdad para tipos implementando Eq y registrándola en Kotest.

Instancias personalizadas de Eq

Veamos un ejemplo de creación de una instancia personalizada de Eq para comparar objetos Foo. Primero, la definición de Foo:

data class Foo(val value: String)

Luego implementamos la typeclass Eq con la lógica de igualdad deseada, devolviendo un EqResult que puede ser Success o Failure:

Aquí estamos diciendo que si un Foo contiene el string hello y el otro contiene world, entonces son iguales. Para devolver un mensaje de error usamos AssertionErrorBuilder, un ayudante para construir el AssertionError concreto apropiado según la plataforma de ejecución.

object FooEq : Eq<Foo> {
override fun equals(actual: Foo, expected: Foo, context: EqContext): EqResult {
return if (actual.value == "hello" && expected.value == "world")
EqResult.Success
else EqResult.Failure {
AssertionErrorBuilder.create().withMessage("I don't like foo").build()
}
}
}
consejo

Si especificamos los valores esperado y real al constructor de errores, el enlace <click to see difference> se generará automáticamente.

Then we register it with Kotest, specifying the type that we want to use it for. Here we are using project config to set it up before any tests are run. We could do this at the spec level too, but bear in mind that this registration is global, so if you are running tests in parallel a per-spec registration will be non-deterministic. See per-call overrides with withEqs below for a parallel-safe alternative scoped to a single comparison.

class ProjectConfig : AbstractProjectConfig() {
override suspend fun beforeProject() {
DefaultEqResolver.register(Foo::class, FooEq)
}
}

Finalmente, podemos usar nuestra instancia personalizada de Eq en las pruebas simplemente usando shouldBe o shouldNotBe normalmente.

test("custom eq should be selected if both sides are the same type") {
Foo("hello") shouldBe Foo("world")
}
nota

Las instancias personalizadas de Eq solo se seleccionan si ambos lados de la llamada son exactamente del tipo especificado al registrarse. Los subtipos no se seleccionan automáticamente y deben registrarse por separado.

Per-call overrides with withEqs

The global DefaultEqResolver.register(...) API mutates a shared map, so it cannot safely express "this BigDecimal comparison should ignore scale, but the next one should not" — especially when specs run in parallel. withEqs provides per-call Eq overrides that are scoped to a single comparison and never touch the global resolver.

val a = BigDecimal("3.14")
val b = BigDecimal("3.140")

a shouldNotBe b // default Eq compares with scale

a withEqs {
register<BigDecimal>(BigDecimalIgnoreScaleEq)
} shouldBe b // ignores scale only for this line

a shouldNotBe b // back to default

Overrides propagate through nested collection, map and data-class comparisons because they ride on the EqContext that's threaded through every recursive Eq call:

val rates = mapOf("USD" to BigDecimal("3.14"), "EUR" to BigDecimal("2.99"))
val expected = mapOf("USD" to BigDecimal("3.140"), "EUR" to BigDecimal("2.990"))

rates withEqs {
register<BigDecimal>(BigDecimalIgnoreScaleEq)
} shouldBe expected

An override only fires when the runtime types of actual and expected match, mirroring the type-match guard used by the global resolver.

withEqs also composes with shouldNotBe for negative comparisons under the same overrides:

val a = BigDecimal("1.00")
val b = BigDecimal("2")

a withEqs {
register<BigDecimal>(BigDecimalIgnoreScaleEq)
} shouldNotBe b