Matching By Field
Whenever we want to match only some of the fields, excluding some other fields from comparison, we should use shouldBeEqualUsingFields
:
val expected = Thing(name = "apple", createdAt = Instant.now())
val actual = Thing(name = "apple", createdAt = Instant.now().plusMillis(42L))
expected shouldBeEqualUsingFields {
excludedProperties = setOf(Thing::createdAt)
actual
}
Likewise, we can explicitly say which fields to match on, and all other fields will be excluded:
val expected = Thing(name = "apple", createdAt = Instant.now())
val actual = Thing(name = "apple", createdAt = Instant.now().plusMillis(42L))
expected shouldBeEqualUsingFields {
includedProperties = setOf(Thing::name)
actual
}
For nested classes, comparison goes recursively, as follows:
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">"""
But we can explicitly stop recursive comparison. In the following example, we are comparing instances of Doctor
class as a whole, not comparing their individual fields. So the difference in mainHospital.mainDoctor
is detected, as opposed to detected differences in mainHospital.mainDoctor.name
in the previous example:
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 =>
Also we can provide custom matchers for fields. In the following example we are matching SimpleDataClass::name
as case-insensitive strings:
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 provides the following override matchers:
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
}
Building Your Own Override Matcher​
Implement Assertable
interface:
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
}
}
For instance, here is the implementation of matchListsIgnoringOrder
:
fun<T> matchListsIgnoringOrder() = Assertable { expected: Any?, actual: Any? ->
customComparison<List<T>>(expected, actual) { expected: List<T>, actual: List<T> ->
actual shouldContainExactlyInAnyOrder expected
}
}
We can use any of Kotest's should***
assertions.