Ir al contenido principal
Versión: 6.1

Comparación por campos

[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 →

Cuando queramos comparar solo algunos campos, excluyendo otros de la comparación, debemos usar shouldBeEqualUsingFields:

   val expected = Thing(name = "apple", createdAt = Instant.now())
val actual = Thing(name = "apple", createdAt = Instant.now().plusMillis(42L))
actual shouldBeEqualUsingFields {
excludedProperties = setOf(Thing::createdAt)
expected
}

Del mismo modo, podemos indicar explícitamente qué campos comparar, y todos los demás campos quedarán excluidos:

   val expected = Thing(name = "apple", createdAt = Instant.now())
val actual = Thing(name = "apple", createdAt = Instant.now().plusMillis(42L))
actual shouldBeEqualUsingFields {
includedProperties = setOf(Thing::name)
expected
}

Para clases anidadas, la comparación se realiza de forma recursiva, como se muestra a continuación:

         val doctor1 = Doctor("billy", 23, emptyList())
val doctor2 = Doctor("barry", 23, emptyList())

val city = City("test1", Hospital("test-hospital1", doctor1))
val city2 = City("test2", Hospital("test-hospital2", doctor2))

shouldThrowAny {
city.shouldBeEqualUsingFields {
city2
}
}.message shouldContain """Using fields:
- mainHospital.mainDoctor.age
- mainHospital.mainDoctor.name
- mainHospital.name
- name

Fields that differ:
- mainHospital.mainDoctor.name => expected:<"barry"> but was:<"billy">
- mainHospital.name => expected:<"test-hospital2"> but was:<"test-hospital1">
- name => expected:<"test2"> but was:<"test1">"""

Pero podemos detener explícitamente la comparación recursiva. En el siguiente ejemplo, estamos comparando instancias de la clase Doctor como un todo, sin comparar sus campos individuales. Por lo tanto, se detecta la diferencia en mainHospital.mainDoctor, a diferencia de las diferencias detectadas en mainHospital.mainDoctor.name en el ejemplo anterior:

         val doctor1 = Doctor("billy", 22, emptyList())
val doctor2 = Doctor("billy", 22, emptyList())

val city = City("test", Hospital("test-hospital", doctor1))
val city2 = City("test", Hospital("test-hospital", doctor2))

shouldFail {
city.shouldBeEqualUsingFields {
useDefaultShouldBeForFields = listOf(Doctor::class)
city2
}
}.message shouldContain """Using fields:
- mainHospital.mainDoctor
- mainHospital.name
- name

Fields that differ:
- mainHospital.mainDoctor =>

También podemos proporcionar matchers personalizados para campos. En el siguiente ejemplo, estamos comparando SimpleDataClass::name como cadenas sin distinguir entre mayúsculas y minúsculas:

     val expected = SimpleDataClass("apple", 1.0, LocalDateTime.now())
val actual = expected.copy(name = "Apple")
shouldThrow<AssertionError> {
actual shouldBeEqualUsingFields expected
}.message.shouldContainInOrder(
"Fields that differ:",
"""- name => expected:<"apple"> but was:<"Apple">""",
)
actual shouldBeEqualUsingFields {
overrideMatchers = mapOf(
SimpleDataClass::name to matchStringsIgnoringCase
)
expected
}

Kotest proporciona los siguientes matchers de reemplazo:

matchBigDecimalsIgnoringScale

 val expected = WithManyFields(
BigDecimal.ONE,
LocalDateTime.now(),
ZonedDateTime.now(),
OffsetDateTime.now(),
Instant.now()
)
val actual = expected.copy(bigDecimal = BigDecimal("1.000"))

actual shouldBeEqualUsingFields {
overrideMatchers = mapOf(
WithManyFields::bigDecimal to matchBigDecimalsIgnoringScale()
)
expected
}

matchDoublesWithTolerance

      val expected = SimpleDataClass("apple", 1.0, LocalDateTime.now())
val actual = expected.copy(weight = 1.001)

actual shouldBeEqualUsingFields {
overrideMatchers = mapOf(
SimpleDataClass::weight to matchDoublesWithTolerance(0.01)
)
expected
}

matchInstantsWithTolerance

val expected = WithManyFields(
BigDecimal.ONE,
LocalDateTime.now(),
ZonedDateTime.now(),
OffsetDateTime.now(),
Instant.now()
)
val actual = expected.copy(instant = expected.instant.plusSeconds(1))

actual shouldBeEqualUsingFields {
overrideMatchers = mapOf(
WithManyFields::instant to matchInstantsWithTolerance(2.seconds)
)
expected
}

matchListsIgnoringOrder

     val expected = DataClassWithList("name", listOf(1, 2, 3))
val actual = expected.copy(elements = listOf(3, 2, 1))
actual shouldBeEqualUsingFields {
overrideMatchers = mapOf(
DataClassWithList::elements to matchListsIgnoringOrder<Int>()
)
expected
}

matchLocalDateTimesWithTolerance

val expected = WithManyFields(
BigDecimal.ONE,
LocalDateTime.now(),
ZonedDateTime.now(),
OffsetDateTime.now(),
Instant.now()
)
val actual = expected.copy(localDateTime = expected.localDateTime.plusSeconds(1))

actual shouldBeEqualUsingFields {
overrideMatchers = mapOf(
WithManyFields::localDateTime to matchLocalDateTimesWithTolerance(2.seconds)
)
expected
}

matchOffsetDateTimesWithTolerance

val expected = WithManyFields(
BigDecimal.ONE,
LocalDateTime.now(),
ZonedDateTime.now(),
OffsetDateTime.now(),
Instant.now()
)
val actual = expected.copy(offsetDateTime = expected.offsetDateTime.plusSeconds(1))

actual shouldBeEqualUsingFields {
overrideMatchers = mapOf(
WithManyFields::offsetDateTime to matchOffsetDateTimesWithTolerance(2.seconds)
)
expected
}

matchStringsIgnoringCase

   val expected = SimpleDataClass("apple", 1.0, LocalDateTime.now())
val actual = expected.copy(name = "Apple")

actual shouldBeEqualUsingFields {
overrideMatchers = mapOf(
SimpleDataClass::name to matchStringsIgnoringCase
)
expected
}

matchZonedDateTimesWithTolerance

val expected = WithManyFields(
BigDecimal.ONE,
LocalDateTime.now(),
ZonedDateTime.now(),
OffsetDateTime.now(),
Instant.now()
)
val actual = expected.copy(zonedDateTime = expected.zonedDateTime.plusSeconds(1))

actual shouldBeEqualUsingFields {
overrideMatchers = mapOf(
WithManyFields::zonedDateTime to matchOffsetDateTimesWithTolerance(2.seconds)
)
expected
}

Creación de su propio matcher de reemplazo

Implemente la interfaz Assertable:

fun interface Assertable {
fun assert(expected: Any?, actual: Any?): CustomComparisonResult
}

sealed interface CustomComparisonResult {
val comparable: Boolean
data object NotComparable: CustomComparisonResult {
override val comparable = false
}
data object Equal: CustomComparisonResult {
override val comparable = true
}
data class Different(val assertionError: AssertionError): CustomComparisonResult {
override val comparable = true
}
}

Por ejemplo, aquí está la implementación de matchListsIgnoringOrder:

fun<T> matchListsIgnoringOrder() = Assertable { expected: Any?, actual: Any? ->
customComparison<List<T>>(expected, actual) { expected: List<T>, actual: List<T> ->
actual shouldContainExactlyInAnyOrder expected
}
}

Podemos usar cualquiera de las aserciones should*** de Kotest.