Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added containsOnce and containsAllOnce (#399) #419

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased]

### Added
- Added `containsOnce` and `containsAllOnce` for `Iterable`
- Added `first` and `single` assertion for `Iterable`

## [0.25] 2021-09-12
Expand Down Expand Up @@ -425,4 +426,4 @@ collections (ex: `Set`).
- Fix issue with isEqualTo and nullable java strings

## [0.9] - 2017-09-25
- Initial Release
- Initial Release
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ semantics as follows:
|containsExactlyInAnyOrder|Asserts the iterable contains **exactly the expected elements**, in **any order**. Each value in expected must correspond to a matching value in actual, and visa-versa.|
|containsExactly|Asserts the list contains **exactly the expected elements**. They must be in the **same order** and there must not be any extra elements.|
|containsNone|Asserts the iterable **does not contain any** of the expected elements|
|containsOnce|Asserts the iterable contains expected element **exactly once**.|
|containsAllOnce|Asserts the iterable contains all the expected elements **exactly once**, in any order. The collection may also contain additional elements. **Duplicate values** in expected elements are ignored.|

### Extracting data

Expand Down
46 changes: 45 additions & 1 deletion assertk/src/commonMain/kotlin/assertk/assertions/iterable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import assertk.assertions.support.show
/**
* Asserts the iterable contains the expected element, using `in`.
* @see [doesNotContain]
* @see [containsOnce]
*/
fun Assert<Iterable<*>>.contains(element: Any?) = given { actual ->
if (element in actual) return
Expand Down Expand Up @@ -36,12 +37,26 @@ fun Assert<Iterable<*>>.containsNone(vararg elements: Any?) = given { actual ->
expected("to contain none of:${show(elements)} but was:${show(actual)}\n elements not expected:${show(notExpected)}")
}

/**
* Asserts the iterable contains expected element exactly once.
* @see [contains]
* @see [containsAllOnce]
*/
fun Assert<Iterable<*>>.containsOnce(element: Any?) = given { actual ->
val count = actual.count { it == element }
if (count == 1) {
return
}
expected("to contain:${show(element)} just once but was:${show(actual)}\n occurrence count:${show(count)}")
}

/**
* Asserts the iterable contains all the expected elements, in any order. The collection may also
* contain additional elements.
* @see [containsNone]
* @see [containsExactly]
* @see [containsOnly]
* @see [containsAllOnce]
*/
fun Assert<Iterable<*>>.containsAll(vararg elements: Any?) = given { actual ->
val notFound = elements.filterNot { it in actual }
Expand All @@ -51,6 +66,35 @@ fun Assert<Iterable<*>>.containsAll(vararg elements: Any?) = given { actual ->
expected("to contain all:${show(elements)} but was:${show(actual)}\n elements not found:${show(notFound)}")
}

/**
* Asserts the iterable contains all the expected elements exactly once, in any order. The collection
* may also contain additional elements. Duplicate values in expected elements are ignored.
* @see [containsOnce]
* @see [containsAll]
*
*/
fun Assert<Iterable<*>>.containsAllOnce(vararg elements: Any?) = given { actual ->
val toFind = elements.toSet()
val notFound = toFind.toMutableSet()
val tooNumerous = mutableListOf<Any?>()
actual.forEach {
if(toFind.contains(it) && !notFound.remove(it)) {
tooNumerous.add(it)
}
}
if (notFound.isEmpty() && tooNumerous.isEmpty()) {
return
}
expected(StringBuilder("to contain:${show(elements)} just once but was:${show(actual)}").apply {
if (notFound.isNotEmpty()) {
append("\n elements not found:${show(notFound)}")
}
if (tooNumerous.isNotEmpty()) {
append("\n elements too numerous:${show(tooNumerous)}")
}
}.toString())
}

/**
* Asserts the iterable contains only the expected elements, in any order. Duplicate values
* in the expected and actual are ignored.
Expand Down Expand Up @@ -323,4 +367,4 @@ fun <E, T : Iterable<E>> Assert<T>.single(): Assert<E> {
expected("to have single element but was empty")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package test.assertk.assertions
import assertk.all
import assertk.assertThat
import assertk.assertions.*
import assertk.assertions.support.fail
import test.assertk.opentestPackageName
import kotlin.test.Test
import kotlin.test.assertEquals
Expand Down Expand Up @@ -51,7 +50,33 @@ class IterableTest {
""".trimMargin(), error.message
)
}
//region
//endregion

//region containsOnce
@Test fun containsOnce_element_present_once_passes() {
assertThat(listOf(1, 1, 2) as Iterable<Int>).containsOnce(2)
}

@Test fun containsOnce_element_not_present_fails() {
val error = assertFails {
assertThat(listOf(1, 2) as Iterable<Int>).containsOnce(3)
}
assertEquals(
"""expected to contain:<3> just once but was:<[1, 2]>
| occurrence count:<0>
""".trimMargin(), error.message)
}

@Test fun containsOnce_element_too_numerous_fails() {
val error = assertFails {
assertThat(listOf(1, 2, 1) as Iterable<Int>).containsOnce(1)
}
assertEquals(
"""expected to contain:<1> just once but was:<[1, 2, 1]>
| occurrence count:<2>
""".trimMargin(), error.message)
}
//endregion

//region containsAll
@Test fun containsAll_all_elements_passes() {
Expand All @@ -70,6 +95,44 @@ class IterableTest {
}
//endregion

//region containsAllOnce
@Test fun containsAllOnce_all_elements_passes() {
assertThat(listOf(1, 2) as Iterable<Int>).containsAll(2, 1)
}


@Test fun containsAllOnce_not_found_elements_fails() {
val error = assertFails {
assertThat(listOf(1, 2) as Iterable<Int>).containsAllOnce(3, 4)
}
assertEquals(
"""expected to contain:<[3, 4]> just once but was:<[1, 2]>
| elements not found:<[3, 4]>
""".trimMargin(), error.message)
}

@Test fun containsAllOnce_duplicate_elements_fails() {
val error = assertFails {
assertThat(listOf(1, 2, 1, 2, 3, 3) as Iterable<Int>).containsAllOnce(1, 2)
}
assertEquals(
"""expected to contain:<[1, 2]> just once but was:<[1, 2, 1, 2, 3, 3]>
| elements too numerous:<[1, 2]>
""".trimMargin(), error.message)
}

@Test fun containsAllOnce_not_found_and_duplicate_elements_fails() {
val error = assertFails {
assertThat(listOf(1, 3, 3) as Iterable<Int>).containsAllOnce(2, 3)
}
assertEquals(
"""expected to contain:<[2, 3]> just once but was:<[1, 3, 3]>
| elements not found:<[2]>
| elements too numerous:<[3]>
""".trimMargin(), error.message)
}
//endregion

//region containsOnly
@Test fun containsOnly_only_elements_passes() {
assertThat(listOf(1, 2) as Iterable<Int>).containsOnly(2, 1)
Expand Down Expand Up @@ -508,4 +571,4 @@ class IterableTest {
//endregion

data class Thing(val one: String, val two: Int, val three: Char)
}
}