
jvmMain.io.kotest.matchers.equality.reflection.kt Maven / Gradle / Ivy
package io.kotest.matchers.equality
import io.kotest.assertions.eq.eq
import io.kotest.assertions.show.show
import io.kotest.matchers.Matcher
import io.kotest.matchers.MatcherResult
import io.kotest.matchers.should
import io.kotest.matchers.shouldNot
import kotlin.reflect.KProperty
import kotlin.reflect.KVisibility
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaField
/**
* Asserts that this is equal to [other] using specific fields
*
* Verifies that [this] instance is equal to [other] using only some specific fields. This is useful for matching
* on objects that contain unknown values, such as a database Entity that contains an ID (you don't know this ID, and it
* doesn't matter for you, for example)
*
* Opposite of [shouldNotBeEqualToUsingFields]
*
* Example:
* ```
* data class Foo(val id: Int, val description: String)
*
* val firstFoo = Foo(1, "Bar!")
* val secondFoo = Foo(2, "Bar!")
*
* firstFoo.shouldBeEqualUsingFields(secondFoo, Foo::description) // Assertion passes
*
* firstFoo shouldBe secondFoo // Assertion fails, `equals` is false!
* ```
*
* Note: Throws [IllegalArgumentException] if [properties] contains any non public property
*
*/
fun T.shouldBeEqualToUsingFields(other: T, vararg properties: KProperty<*>) {
this should beEqualToUsingFields(other, *properties)
}
/**
* Asserts that this is NOT equal to [other] using specific fields
*
* Verifies that [this] instance is not equal to [other] using only some specific fields. This is useful for matching
* on objects that contain unknown values, such as a database Entity that contains an ID (you don't know this ID, and it
* doesn't matter for you, for example)
*
* Opposite of [shouldBeEqualToUsingFields]
*
* Example:
* ```
* data class Foo(val id: Int, val description: String)
*
* val firstFoo = Foo(1, "Bar!")
* val secondFoo = Foo(2, "BAT")
*
* firstFoo.shouldNotBeEqualToUsingFields(secondFoo, Foo::description) // Assertion passes
*
* ```
* Note: Throws [IllegalArgumentException] if [properties] contains any non public property
*
*
* @see [beEqualToUsingFields]
* @see [shouldNotBeEqualToIgnoringFields]
*
*/
fun T.shouldNotBeEqualToUsingFields(other: T, vararg properties: KProperty<*>) {
this shouldNot beEqualToUsingFields(other, *properties)
}
/**
* Matcher that compares values using specific fields
*
* Verifies that two instances are equal considering only some specific fields. This is useful for matching on objects
* that contain unknown values, such as a database Entity that contains an ID (you don't know this ID, and it doesn't
* matter for you, for example). However, if no fields are specified, all public fields are considered.
*
*
* Example:
* ```
* data class Foo(val id: Int, val description: String)
*
* val firstFoo = Foo(1, "Bar!")
* val secondFoo = Foo(2, "Bar!")
*
* firstFoo should beEqualToUsingFields(secondFoo, Foo::description) // Assertion passes
*
* ```
*
* Note: Throws [IllegalArgumentException] if [fields] contains any non public property
*
* @see [shouldBeEqualToUsingFields]
* @see [shouldNotBeEqualToUsingFields]
* @see [beEqualToIgnoringFields]
*
*/
fun beEqualToUsingFields(other: T, vararg fields: KProperty<*>): Matcher = object : Matcher {
override fun test(value: T): MatcherResult {
val hasNonPublicFields = fields.any { it.visibility != KVisibility.PUBLIC }
if (hasNonPublicFields) {
throw IllegalArgumentException("Only fields of public visibility are allowed to be use for used for checking equality")
}
val fieldsToBeConsidered: List> = fields.toList().takeUnless { it.isEmpty() }
?: value::class.memberProperties.filter { it.visibility == KVisibility.PUBLIC }
val failed = checkEqualityOfFields(fieldsToBeConsidered, value, other)
val fieldsString = fields.joinToString(", ", "[", "]") { it.name }
return MatcherResult(
failed.isEmpty(),
{ "$value should be equal to $other using fields $fieldsString; Failed for $failed" },
{
"$value should not be equal to $other using fields $fieldsString"
})
}
}
/**
* Asserts that this is equal to [other] without using specific fields
*
* Verifies that [this] instance is equal to [other] without using some specific fields. This is useful for matching
* on objects that contain unknown values, such as a database Entity that contains an ID (you don't know this ID, and it
* doesn't matter for you, for example)
*
* Opposite of [shouldNotBeEqualToIgnoringFields]
*
* Example:
* ```
* data class Foo(val id: Int, val description: String)
*
* val firstFoo = Foo(1, "Bar!")
* val secondFoo = Foo(2, "Bar!")
*
* firstFoo.shouldBeEqualToIgnoringFields(secondFoo, Foo::id) // Assertion passes
*
* firstFoo shouldBe secondFoo // Assertion fails, `equals` is false!
* ```
*
*/
fun T.shouldBeEqualToIgnoringFields(other: T, property: KProperty<*>, vararg others: KProperty<*>) {
this should beEqualToIgnoringFields(other = other, ignorePrivateFields = true, property = property, others = others)
}
/**
* Asserts that this is equal to [other] without using specific fields
*
* Verifies that [this] instance is equal to [other] without using some specific fields and ignoring/not-ignoring
* private fields.
* This is useful for matching on objects that contain unknown values, such as a database Entity that contains an
* ID (you don't know this ID, and it doesn't matter for you, for example)
*
* Opposite of [shouldNotBeEqualToIgnoringFields]
*
* Example:
* ```
* data class Foo(val id: Int, val description: String, private val quote: String)
*
* val firstFoo = Foo(1, "Bar!", "Q1")
* val secondFoo = Foo(2, "Bar!", "Q2")
*
* firstFoo.shouldBeEqualToIgnoringFields(other = secondFoo, ignorePrivateFields = true , properties = Foo::id) // Assertion passes
* firstFoo.shouldBeEqualToIgnoringFields(other = secondFoo, ignorePrivateFields = false , properties = Foo::id) // Assertion fails
*
* firstFoo shouldBe secondFoo // Assertion fails, `equals` is false!
* ```
*
*/
fun T.shouldBeEqualToIgnoringFields(
other: T,
ignorePrivateFields: Boolean,
property: KProperty<*>,
vararg others: KProperty<*>,
) {
this should beEqualToIgnoringFields(
other = other,
ignorePrivateFields = ignorePrivateFields,
property = property,
others = others
)
}
/**
* Asserts that this is not equal to [other] without using specific fields
*
* Verifies that [this] instance is not equal to [other] without using some specific fields. This is useful for matching
* on objects that contain unknown values, such as a database Entity that contains an ID (you don't know this ID, and it
* doesn't matter for you, for example)
*
* Opposite of [shouldBeEqualToIgnoringFields]
*
* Example:
* ```
* data class Foo(val id: Int, val description: String)
*
* val firstFoo = Foo(1, "Bar!")
* val secondFoo = Foo(2, "BAT!")
*
* firstFoo.shouldNotBeEqualToIgnoringFields(secondFoo, Foo::id) // Assertion passes
* ```
*
*/
fun T.shouldNotBeEqualToIgnoringFields(other: T, property: KProperty<*>, vararg others: KProperty<*>) =
this shouldNot beEqualToIgnoringFields(
other = other,
ignorePrivateFields = true,
property = property,
others = others,
)
/**
* Asserts that this is not equal to [other] without using specific fields
*
* Verifies that [this] instance is not equal to [other] without using some specific fields and ignoring/not-ignoring
* private fields.
* This is useful for matching on objects that contain unknown values, such as a database Entity that contains an ID (you don't know this ID, and it
* doesn't matter for you, for example)
*
* Opposite of [shouldBeEqualToIgnoringFields]
*
* Example:
* ```
* data class Foo(val id: Int, val description: String, private val quote: String)
*
* val firstFoo = Foo(1, "Bar!", "Q1")
* val secondFoo = Foo(2, "Bar!", "Q2")
*
* firstFoo.shouldNotBeEqualToIgnoringFields(other = secondFoo, ignorePrivateFields = false, properties = Foo::id) // Assertion passes
* firstFoo.shouldNotBeEqualToIgnoringFields(other = secondFoo, ignorePrivateFields = true, properties = Foo::id) // Assertion fails
* ```
*
*/
fun T.shouldNotBeEqualToIgnoringFields(
other: T,
ignorePrivateFields: Boolean,
property: KProperty<*>,
vararg others: KProperty<*>
) =
this shouldNot beEqualToIgnoringFields(
other = other,
ignorePrivateFields = ignorePrivateFields,
property = property,
others = others,
)
/**
* Matcher that compares values without using specific fields
*
* Verifies that two instances are equal by not using only some specific fields. This is useful for matching
* on objects that contain unknown values, such as a database Entity that contains an ID (you don't know this ID, and it
* doesn't matter for you, for example)
*
*
* Example:
* ```
* data class Foo(val id: Int, val description: String)
*
* val firstFoo = Foo(1, "Bar!")
* val secondFoo = Foo(2, "Bar!")
*
* firstFoo should beEqualToIgnoringFields(secondFoo, Foo::id) // Assertion passes
*
* ```
*
* @see [beEqualToUsingFields]
* @see [shouldBeEqualToIgnoringFields]
* @see [shouldNotBeEqualToIgnoringFields]
*
*/
fun beEqualToIgnoringFields(
other: T,
ignorePrivateFields: Boolean,
property: KProperty<*>,
vararg others: KProperty<*>
): Matcher = object : Matcher {
override fun test(value: T): MatcherResult {
val fields = listOf(property) + others
val fieldNames = fields.map { it.name }
val fieldsExcludingGivenFields = value::class.memberProperties
.filterNot { fieldNames.contains(it.name) }
val fieldsToBeConsidered: List> = if (ignorePrivateFields) {
fieldsExcludingGivenFields.filter { it.visibility == KVisibility.PUBLIC }
} else {
fieldsExcludingGivenFields.onEach { it.isAccessible = true }
}
val failed = checkEqualityOfFields(fieldsToBeConsidered, value, other)
val fieldsString = fields.joinToString(", ", "[", "]") { it.name }
return MatcherResult(
failed.isEmpty(),
{ "$value should be equal to $other ignoring fields $fieldsString; Failed for $failed" },
{
"$value should not be equal to $other ignoring fields $fieldsString"
})
}
}
fun T.shouldBeEqualToComparingFields(
other: T,
ignorePrivateFields: Boolean = true,
ignoreComputedFields: Boolean = true
) {
this should beEqualComparingFields(other, ignorePrivateFields, emptyList(), ignoreComputedFields)
}
fun T.shouldBeEqualToComparingFieldsExcept(
other: T,
ignorePrivateFields: Boolean,
ignoreProperty: KProperty<*>,
vararg ignoreProperties: KProperty<*>,
ignoreComputedFields: Boolean = true
) {
this should beEqualComparingFields(
other,
ignorePrivateFields,
listOf(ignoreProperty) + ignoreProperties,
ignoreComputedFields
)
}
fun T.shouldNotBeEqualToComparingFieldsExcept(
other: T,
ignorePrivateFields: Boolean,
ignoreProperty: KProperty<*>,
vararg ignoreProperties: KProperty<*>,
includeComputedProperties: Boolean = false
) {
this shouldNot beEqualComparingFields(
other,
ignorePrivateFields,
listOf(ignoreProperty) + ignoreProperties,
includeComputedProperties
)
}
fun T.shouldBeEqualToComparingFieldsExcept(
other: T,
ignoreProperty: KProperty<*>,
vararg ignoreProperties: KProperty<*>
) {
this should beEqualComparingFields(other, true, listOf(ignoreProperty) + ignoreProperties, true)
}
fun T.shouldNotBeEqualToComparingFieldsExcept(
other: T,
ignoreProperty: KProperty<*>,
vararg ignoreProperties: KProperty<*>
) {
this should beEqualComparingFields(other, true, listOf(ignoreProperty) + ignoreProperties, true)
}
infix fun T.shouldBeEqualToComparingFields(other: T) {
this.shouldBeEqualToComparingFields(other, true)
}
infix fun T.shouldNotBeEqualToComparingFields(other: T) {
this shouldNot beEqualComparingFields(other, true, emptyList(), true)
}
fun T.shouldNotBeEqualToComparingFields(
other: T,
ignorePrivateFields: Boolean = true,
ignoreComputedFields: Boolean = true
) {
this shouldNot beEqualComparingFields(other, ignorePrivateFields, emptyList(), ignoreComputedFields)
}
private typealias PropertyPredicate = (KProperty<*>) -> Boolean
// If no java field exists, it is a computed property which only has a getter
private val nonComputed: PropertyPredicate = { it.javaField != null }
private val nonPrivate: PropertyPredicate = { it.visibility != KVisibility.PRIVATE }
private infix fun PropertyPredicate.and(other: PropertyPredicate) =
{ property: KProperty<*> -> this(property) && other(property) }
fun beEqualComparingFields(
other: T,
ignorePrivateFields: Boolean,
propertiesToExclude: List>,
ignoreComputedFields: Boolean,
) = object : Matcher {
override fun test(value: T): MatcherResult {
val predicates = listOfNotNull(
if (ignorePrivateFields) nonPrivate else null,
if (ignoreComputedFields) nonComputed else null,
{ it !in propertiesToExclude }
).reduce { a, b -> a and b }
val fieldsToCompare = value::class.memberProperties
.asSequence()
.onEach { it.isAccessible = true }
.filter(predicates)
.sortedBy { it.name }
.toList()
val failed = checkEqualityOfFields(fieldsToCompare, value, other)
return MatcherResult(
failed.isEmpty(),
{
"""Expected ${value.show().value} to equal ${other.show().value}
| Using fields: ${fieldsToCompare.joinToString(", ") { it.name }}
| Value differ at:
| ${failed.withIndex().joinToString("\n") { "${it.index + 1}) ${it.value}" }}
""".trimMargin()
},
{
"""Expected ${value.show().value} to not equal ${other.show().value}
| Using fields: ${fieldsToCompare.joinToString(", ") { it.name }}
""".trimMargin()
}
)
}
}
private fun checkEqualityOfFields(fields: List>, value: T, other: T): List {
return fields.mapNotNull {
val actual = it.getter.call(value)
val expected = it.getter.call(other)
val isEqual = eq(actual, expected) == null
if (isEqual) null else "${it.name}: ${actual.show().value} != ${expected.show().value}"
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy