Skip to content

Commit

Permalink
add search for similar items to containAll (#3909)
Browse files Browse the repository at this point in the history
add search for similar items to `containAll`, which provides more
insight into what exactly is not matched, for instance:
```
            shouldThrowAny {
               listOf(sweetGreenApple, sweetGreenPear, sourYellowLemon).shouldContainAll(
                  listOf(sweetGreenApple, sweetRedApple)
               )
            }.shouldHaveMessage("""
               |Collection should contain all of [Fruit(name=apple, color=green, taste=sweet), Fruit(name=apple, color=red, taste=sweet)] but was missing [Fruit(name=apple, color=red, taste=sweet)]Possible matches:
               | expected: Fruit(name=apple, color=green, taste=sweet),
               |  but was: Fruit(name=apple, color=red, taste=sweet),
               |  The following fields did not match:
               |    "color" expected: <"green">, but was: <"red">
    """.trimMargin())
```

Co-authored-by: Sam <sam@sksamuel.com>
  • Loading branch information
AlexCue987 and sksamuel committed Mar 10, 2024
1 parent 3d46d16 commit bad750c
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 1 deletion.
Expand Up @@ -6,6 +6,7 @@ import io.kotest.matchers.Matcher
import io.kotest.matchers.MatcherResult
import io.kotest.matchers.should
import io.kotest.matchers.shouldNot
import io.kotest.similarity.possibleMatchesForSet

fun <T> Iterable<T>.shouldContainAll(vararg ts: T) = toList().shouldContainAll(*ts)
fun <T> Array<T>.shouldContainAll(vararg ts: T) = asList().shouldContainAll(*ts)
Expand Down Expand Up @@ -38,8 +39,10 @@ fun <T> containAll(
}
val passed = missing.isEmpty()

val possibleMatchesDescription = possibleMatchesForSet(passed, value.toSet(), missing.toSet(), verifier)

val failure =
{ "Collection should contain all of ${ts.print().value} but was missing ${missing.print().value}" }
{ "Collection should contain all of ${ts.print().value} but was missing ${missing.print().value}$possibleMatchesDescription" }
val negFailure = { "Collection should not contain all of ${ts.print().value}" }

return MatcherResult(passed, failure, negFailure)
Expand Down
@@ -1,11 +1,13 @@
package com.sksamuel.kotest.matchers.collections

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.assertions.throwables.shouldThrowAny
import io.kotest.core.spec.style.WordSpec
import io.kotest.matchers.collections.containAll
import io.kotest.matchers.collections.shouldContainAll
import io.kotest.matchers.collections.shouldNotContainAll
import io.kotest.matchers.should
import io.kotest.matchers.string.shouldEndWith
import io.kotest.matchers.throwable.shouldHaveMessage

class ShouldContainAllTest : WordSpec() {
Expand Down Expand Up @@ -117,6 +119,38 @@ class ShouldContainAllTest : WordSpec() {
listOf<Number>(1, 2).shouldContainAll(listOf<Number>(1L, 2L))
}.shouldHaveMessage("""Collection should contain all of [1L, 2L] but was missing [1L, 2L]""")
}

"print one possible match for one mismatched element" {
shouldThrowAny {
listOf(sweetGreenApple, sweetGreenPear, sourYellowLemon).shouldContainAll(
listOf(sweetGreenApple, sweetRedApple)
)
}.shouldHaveMessage("""
|Collection should contain all of [Fruit(name=apple, color=green, taste=sweet), Fruit(name=apple, color=red, taste=sweet)] but was missing [Fruit(name=apple, color=red, taste=sweet)]Possible matches:
| expected: Fruit(name=apple, color=green, taste=sweet),
| but was: Fruit(name=apple, color=red, taste=sweet),
| The following fields did not match:
| "color" expected: <"green">, but was: <"red">
""".trimMargin())
}

"print two possible matches for one mismatched element" {
shouldThrowAny {
listOf(sweetRedApple, sweetGreenPear, sourYellowLemon).shouldContainAll(
listOf(sweetGreenApple, sourYellowLemon)
)
}.message.shouldEndWith("""
| expected: Fruit(name=apple, color=red, taste=sweet),
| but was: Fruit(name=apple, color=green, taste=sweet),
| The following fields did not match:
| "color" expected: <"red">, but was: <"green">
|
| expected: Fruit(name=pear, color=green, taste=sweet),
| but was: Fruit(name=apple, color=green, taste=sweet),
| The following fields did not match:
| "name" expected: <"pear">, but was: <"apple">
""".trimMargin())
}
}
}
}
@@ -0,0 +1,14 @@
package com.sksamuel.kotest.matchers.collections

data class Fruit(
val name: String,
val color: String,
val taste: String
)

val sweetGreenApple = Fruit("apple", "green", "sweet")
val sweetRedApple = Fruit("apple", "red", "sweet")
val sweetGreenPear = Fruit("pear", "green", "sweet")
val sourYellowLemon = Fruit("lemon", "yellow", "sour")
val tartRedCherry = Fruit("cherry", "red", "tart")
val bitterPurplePlum = Fruit("plum", "purple", "bitter")
Expand Up @@ -3062,6 +3062,10 @@ public final class io/kotest/matchers/ShouldKt {
public static final fun shouldNotHave (Ljava/lang/Object;Lio/kotest/matchers/Matcher;)V
}

public final class io/kotest/similarity/PossibleMatchesForSetKt {
public static final fun possibleMatchesForSet (ZLjava/util/Set;Ljava/util/Set;Lio/kotest/equals/Equality;)Ljava/lang/String;
}

public final class io/kotest/similarity/PossibleMatchesKt {
public static final fun possibleMatchesDescription (Ljava/util/Set;Ljava/lang/Object;)Ljava/lang/String;
}
Expand Down
@@ -0,0 +1,23 @@
package io.kotest.similarity

import io.kotest.equals.Equality

fun<T> possibleMatchesForSet(
passed: Boolean,
expected: Set<T>,
actual: Set<T>,
verifier: Equality<T>?
): String {
return when {
passed -> ""
actual.isEmpty() -> ""
Equality.default<T>().name() == (verifier?.name() ?: Equality.default<T>().name()) -> {
val possibleMatches = actual.map {
possibleMatchesDescription(expected, it)
}.filter { it.isNotEmpty() }
if(possibleMatches.isEmpty()) ""
else "Possible matches:${possibleMatches.joinToString("\n")}"
}
else -> ""
}
}

0 comments on commit bad750c

Please sign in to comment.