Skip to content

Conversation

habara-k
Copy link
Contributor

@habara-k habara-k commented Apr 18, 2025

This PR resolves #4847.

Changes

Allow emptyList to be passed as an argument to

  • should[Not]ContainAnyOf
  • should[Not]BeOneOf
  • should[Not]BeIn

Minor changes

Motivation

Mathematically, the following assertions should each fail or pass as described:

// Example assertions with empty lists
listOf(...).shouldContainAnyOf(emptyList())    // Expected to fail
listOf(...).shouldNotContainAnyOf(emptyList()) // Expected to pass
(...).shouldBeOneOf(emptyList())               // Expected to fail
(...).shouldNotBeOneOf(emptyList())            // Expected to pass
(...).shouldBeIn(emptyList())                  // Expected to fail
(...).shouldNotBeIn(emptyList())               // Expected to pass

However, when these assertions are passed an empty collection, an error occurs stating: "Asserting content on empty collection. Use Collection.shouldBeEmpty() instead."

These assertions lack consistency with the existing specifications of the following assertions:

listOf(...).shouldContainAll(emptyList())     // Passes
listOf(...).shouldNotContainAll(emptyList())  // Fails
mapOf(...).shouldContainAll(emptyMap())       // Passes
mapOf(...).shouldNotContainAll(emptyMap())    // Fails
mapOf(...).shouldContainKeys()                // Passes
mapOf(...).shouldNotContainKeys()             // Fails
mapOf(...).shouldContainAnyKeysOf()           // Fails
mapOf(...).shouldNotContainAnyKeysOf()        // Passes
mapOf(...).shouldContainValues()              // Passes
mapOf(...).shouldNotContainValues()           // Fails
mapOf(...).shouldContainAnyValuesOf()         // Fails
mapOf(...).shouldNotContainAnyValuesOf()      // Passes

This PR aims to correct the inconsistent behavior and make the Kotest assertions mathematically accurate and more general.

@habara-k habara-k requested a review from a team as a code owner April 18, 2025 12:46
@sksamuel
Copy link
Member

This is going to change the existing behavior of all these matchers right ?

@sksamuel
Copy link
Member

Is there something "official" that indicates what should happen for an empty collection? Like maybe something equivalent in the Kotlin SDK ?

@AlexCue987
Copy link
Member

Is there something "official" that indicates what should happen for an empty collection? Like maybe something equivalent in the Kotlin SDK ?

Kotlin standard library states that:

listOf(1, 2, 3).containsAll(listOf()) shouldBe true

Mathematically this PR make sense to me: shouldNotContainAnyOf means "there are no elements in the collection on the right that are also in the collection on the left, which is true if the collection on the right is empty.

Likewise listOf(...).shouldContainAll(emptyList()) means "there are no elements on the right that are not in the collection on the left" - also true.

So I agree with this PR.

@sksamuel
Copy link
Member

Do you think we should add some explicit docs for the matchers that link to why empty collections evaluate to true ?

@AlexCue987
Copy link
Member

Do you think we should add some explicit docs for the matchers that link to why empty collections evaluate to true ?

if we pivot on this approach, then yes I agree, we should explain it in some detail. because this is a breaking change

@habara-k
Copy link
Contributor Author

Thank you for your response!

Since this behavior is mathematically valid and adopted in general libraries, I don't believe it needs to be explained in detail as an exception in the documentation. However, since it is a breaking change, it would be appropriate to increase the major version and announce it as shown in https://kotest.io/docs/changelog.html#breaking-changes-and-removed-deprecated-methods.

Copy link
Member

@AlexCue987 AlexCue987 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this PR. IMO the only one little thing it needs added are short explanations in javadocs

@@ -31,7 +31,7 @@ infix fun <T> T.shouldBeIn(collection: Collection<T>): T {
* Assertion to check that this element is not any of [collection]. This assertion checks by value, and not by reference,
* therefore any instance with same value must not be in [collection], or this will fail.
*
* An empty collection will always fail. If you need to check for empty collection, use [Collection.shouldBeEmpty]
* An empty collection will always pass.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a short explanation why "An empty collection will always pass".

Copy link
Contributor Author

@habara-k habara-k Apr 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the change in 20f3b8e97d2d65.

@@ -64,7 +64,7 @@ fun <T> T.shouldBeIn(vararg any: T): T {
* Assertion to check that this element is not any of [any]. This assertion checks by value, and not by reference,
* therefore any instance with same value must not be in [any], or this will fail.
*
* An empty collection will always fail. If you need to check for empty collection, use [Collection.shouldBeEmpty]
* An empty collection will always pass.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a short explanation why "An empty collection will always pass".

@@ -99,7 +99,7 @@ infix fun <T> T.shouldBeIn(array: Array<T>): T {
* Assertion to check that this element is not any of [array]. This assertion checks by value, and not by reference,
* therefore any instance with same value must not be in [array], or this will fail.
*
* An empty array will always fail. If you need to check for empty array, use [Array.shouldBeEmpty]
* An empty array will always pass.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a short explanation why "An empty collection will always pass".

@@ -29,7 +29,7 @@ infix fun <T> T.shouldBeOneOf(collection: Collection<T>): T {
* Assertion to check that this instance is not in [collection]. This assertion checks by reference, and not by value,
* therefore the exact instance must not be in [collection], or this will fail.
*
* An empty collection will always fail. If you need to check for empty collection, use [Collection.shouldBeEmpty]
* An empty collection will always pass.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a short explanation why "An empty collection will always pass".

@@ -61,7 +61,7 @@ fun <T> T.shouldBeOneOf(vararg any: T): T {
* Assertion to check that this instance is not any of [any]. This assertion checks by reference, and not by value,
* therefore the exact instance must not be in [any], or this will fail.
*
* An empty collection will always fail. If you need to check for empty collection, use [Collection.shouldBeEmpty]
* An empty collection will always pass.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a short explanation why "An empty collection will always pass".

@habara-k habara-k requested a review from AlexCue987 April 22, 2025 14:17
@@ -34,6 +34,7 @@ kotlin {
dependencies {
implementation(projects.kotestProperty)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.test)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what did we add this dependency for?

Copy link
Contributor Author

@habara-k habara-k Apr 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for local testing of :kotest-assertions:kotest-assertions-core.
I would like to develop in a test-driven manner, so the dependency is added first.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for local testing of :kotest-assertions:kotest-assertions-core. I would like to develop in a test-driven manner, so the dependency is added first.

this is an unrelated change. feel free to open a separate PR for that. to keep this PR atomic, please remove the dependency.

other than that, I think the PR is ready to be merged, and is a nice improvement.

habara-k and others added 5 commits April 23, 2025 12:50
The matchers were correct, but the inverted error messages mistakenly
swapped the `value` and `anotherInstant` variables, leading to incorrect
error messages.

I added simple tests asserting the correct messages for the `before` and
`after` matchers.

I would also suggest to align the wording and punctuation (which I did
not do as part of this bug fix - please fix or open an issue in case you
agree):
- "but it's not" is not consistent with the other formulations and
should probably be removed - it is clear from the context (failing
assertion)
- Some error messages end with a ".", some do not

This should probably be merged both into master and 5.9 if a 5.9.2
release should be made. I based the branch on 5.9.1.

---------

Co-authored-by: Alex Kuznetsov <Alex.Cue.987@gmail.com>
According the
[documentation](https://kotest.io/docs/proptest/property-test-generators-list.html),
"`Exhaustive.azstring(1..2)` ... [returns] `a, b, c, ...., yz, zz`". The
`yz` seems to imply that it generates all possible permutations;
however, this doesn't seem to be the case. For example for
`Exhaustive.azstring(2..2)` should produce `aa, ab, ac, ... , zx, zy,
zz` but instead produces `aa, bb, cc, ... , xx, yy, zz`. There is a
mismatch between the code and the documentation.

My changes updates the code to match the written behavior. If the
current behavior is intentional, I can change this pull request to
update the documentation instead (and update the test to match the
current behavior)
Copy link
Member

@AlexCue987 AlexCue987 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very nice!

@AlexCue987 AlexCue987 added this pull request to the merge queue Apr 23, 2025
github-merge-queue bot pushed a commit that referenced this pull request Apr 23, 2025
…t]BeOneOf` and `should[Not]BeIn` (#4849)

<!-- 
If this PR updates documentation, please update all relevant versions of
the docs, see:
https://github.com/kotest/kotest/tree/master/documentation/versioned_docs
The documentation at
https://github.com/kotest/kotest/tree/master/documentation/docs is the
documentation for the next minor or major version _TO BE RELEASED_
-->

This PR resolves #4847.

## Changes

Allow emptyList to be passed as an argument to
- `should[Not]ContainAnyOf`
- `should[Not]BeOneOf`
- `should[Not]BeIn`

### Minor changes
- ~Add dependency of libs.kotlinx.coroutines.test for local testing of
:kotest-assertions:kotest-assertions-core.~ Moved to #4856


## Motivation

Mathematically, the following assertions should each fail or pass as
described:

```kt
// Example assertions with empty lists
listOf(...).shouldContainAnyOf(emptyList())    // Expected to fail
listOf(...).shouldNotContainAnyOf(emptyList()) // Expected to pass
listOf(...).shouldBeOneOf(emptyList())         // Expected to fail
listOf(...).shouldNotBeOneOf(emptyList())      // Expected to pass
listOf(...).shouldBeIn(emptyList())            // Expected to fail
listOf(...).shouldNotBeIn(emptyList())         // Expected to pass
```

However, when these assertions are passed an empty collection, an error
occurs stating: "Asserting content on empty collection. Use
Collection.shouldBeEmpty() instead."

These assertions lack consistency with the existing specifications of
the following assertions:

```kt
listOf(...).shouldContainAll(emptyList())     // Passes
listOf(...).shouldNotContainAll(emptyList())  // Fails
mapOf(...).shouldContainAll(emptyMap())       // Passes
mapOf(...).shouldNotContainAll(emptyMap())    // Fails
mapOf(...).shouldContainKeys()                // Passes
mapOf(...).shouldNotContainKeys()             // Fails
mapOf(...).shouldContainAnyKeysOf()           // Fails
mapOf(...).shouldNotContainAnyKeysOf()        // Passes
mapOf(...).shouldContainValues()              // Passes
mapOf(...).shouldNotContainValues()           // Fails
mapOf(...).shouldContainAnyValuesOf()         // Fails
mapOf(...).shouldNotContainAnyValuesOf()      // Passes
```

This PR aims to correct the inconsistent behavior and make the Kotest
assertions mathematically accurate and more general.

---------

Co-authored-by: Felix Wiemuth <533601+felixwiemuth@users.noreply.github.com>
Co-authored-by: Alex Kuznetsov <Alex.Cue.987@gmail.com>
Co-authored-by: ej-castillo <eugene.j.castillo@gmail.com>
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to no response for status checks Apr 23, 2025
@AlexCue987 AlexCue987 added this pull request to the merge queue Apr 23, 2025
Merged via the queue into kotest:master with commit 39fb30e Apr 24, 2025
15 of 19 checks passed
github-merge-queue bot pushed a commit that referenced this pull request May 5, 2025
…kotest-assertions-core (#4856)

<!-- 
If this PR updates documentation, please update all relevant versions of
the docs, see:
https://github.com/kotest/kotest/tree/master/documentation/versioned_docs
The documentation at
https://github.com/kotest/kotest/tree/master/documentation/docs is the
documentation for the next minor or major version _TO BE RELEASED_
-->

## Changes

- Add dependency of libs.kotlinx.coroutines.test to
:kotest-assertions:kotest-assertions-core

## Motivation

It is necessary to add this dependency to run
`:kotest-assertions:kotest-assertions-core` locally.

## Related PRs

- #4849
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

EmptyList Is Not Supported as an Argument in should[Not]ContainAnyOf
5 participants