Skip to content
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- signature of `isIn` and `isNotIn` has been changed to ensure at least two items are supplied.
- added assertions `isIn(Iterable<T>)` and `isNotIn(Iterable<T>)`

### Added
- Added `doesNotExist` assertions to `Path`
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I moved this note into the added section for consistency

- Added `havingInstancesOf` and `notHavingInstancesOf` for `Iterable` and `Array`

## [0.28.1] 2024-04-17

### Added
Expand Down
28 changes: 28 additions & 0 deletions assertk/src/commonMain/kotlin/assertk/assertions/array.kt
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,34 @@ fun Assert<Array<*>>.containsExactly(vararg elements: Any?) = given { actual ->
expectedListDiff(elements.asList(), actual.asList())
}

/**
* Asserts the collection contains at least one instance of a given type.
*
* ```
* assertThat(arrayOf<Any>("one", "two", 1)).havingInstanceOf<String>().each {
* it.hasLength(3)
* }
* ```
*/
inline fun <reified T> Assert<Array<*>>.havingInstancesOf(): Assert<List<T>> {
return transform("contains instances of ${T::class}") { actual ->
actual.filterIsInstance<T>().also {
if (it.isEmpty()) expected("to contain at least one instance of ${T::class} but was ${actual.asList()}")
}
}
}

/**
* Asserts the collection does not contain an instance of a given type.
*
* ```
* assertThat(arrayOf<Any>("one", "two", 1)).havingInstancesOf<Double>()
* ```
*/
inline fun <reified T> Assert<Array<*>>.notHavingInstancesOf() = given { actual ->
if (actual.any { it is T }) expected("to not contain instances of ${T::class} but was ${actual.asList()}")
}

/**
* Asserts on each item in the array. The given lambda will be run for each item.
*
Expand Down
28 changes: 28 additions & 0 deletions assertk/src/commonMain/kotlin/assertk/assertions/iterable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,34 @@ internal fun MutableList<*>.removeFirst(value: Any?) {
if (index > -1) removeAt(index)
}

/**
* Asserts the collection contains at least one instance of a given type.
*
* ```
* assertThat(listOf<Any>("one", "two", 1)).havingInstanceOf<String>().each {
* it.hasLength(3)
* }
* ```
*/
inline fun <reified T> Assert<Iterable<*>>.havingInstancesOf(): Assert<List<T>> {
return transform("contains instance of ${T::class}") { actual ->
actual.filterIsInstance<T>().also {
Copy link
Contributor

Choose a reason for hiding this comment

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

I would use any here so that it returns true as soon as a single instance is found rather that needing to perform a full iteration and allocate a new list.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The sub-list allocation is necessary to provide a way to continue making assertions against the sublist. No?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, yeah, I missed the new list was propagated as a return value. I'm a bit hesitant about the API shape using a verb "contains" while the return value is a full filtering.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, agreed. I noticed the having discussion after I pushed this. Would havingInstancesOf be better? Maybe the negation would be doesNotHaveInstancesOf?

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess I'm a bit torn on whether this makes sense as a dedicated operator since it combines a transform with an assertion in a way that I don't think has precedent from other operators. We'll see what the actual maintainers think, though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'd argue there's precedent with isNotNull. It's a transform and an assertion.

Copy link
Contributor

@JakeWharton JakeWharton Oct 2, 2024

Choose a reason for hiding this comment

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

But it's atomic in nature. You cannot break it down into discrete steps.

Whereas this one is very clearly a filterIsInstance transformation and then an isNotEmpty assertion.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fair. I'd find a shorthand for the transformation basically just as helpful.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

A transformation without assertion might set a (good) precedent for other collection operators.
@evant, could you weigh in when you have some time?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

single is also an assertion and transformation of the same kind

if (it.isEmpty()) expected("to contain at least one instance of ${T::class} but was $actual")
}
}
}

/**
* Asserts the collection does not contain an instance of a given type.
*
* ```
* assertThat(listOf<Any>("one", "two", 1)).doesNotContainInstanceOf<Double>()
* ```
*/
inline fun <reified T> Assert<Iterable<*>>.notHavingInstancesOf() = given { actual ->
if (actual.any { it is T }) expected("to not contain instances of ${T::class} but was $actual")
}

/**
* Asserts on each item in the iterable. The given lambda will be run for each item.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,32 @@ class ArrayTest {
}
//endregion

//region containsInstanceOf
@Test fun containsInstanceOf_element_present_passes() {
assertThat(arrayOf<Any>(1, "two")).havingInstancesOf<String>().single().isEqualTo("two")
}

@Test fun containsInstanceOf_element_missing_fails() {
val error = assertFailsWith<AssertionError> {
assertThat(arrayOf<Any>(1, "two")).havingInstancesOf<Double>()
}
assertEquals("expected to contain at least one instance of class kotlin.Double but was [1, two]", error.message)
}
//endregion

//region doesNotContainInstanceOf
@Test fun doesNotContainInstanceOf_element_present_fails() {
val error = assertFailsWith<AssertionError>() {
assertThat(arrayOf<Any>(1, "two")).notHavingInstancesOf<String>()
}
assertEquals("expected to not contain instances of class kotlin.String but was [1, two]", error.message)
}

@Test fun doesNotContainInstanceOf_element_missing_passes() {
assertThat(arrayOf<Any>(1, "two")).notHavingInstancesOf<Double>()
}
//endregion

//region each
@Test
fun each_empty_list_passes() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import assertk.assertions.contains
import assertk.assertions.containsAtLeast
import assertk.assertions.containsExactly
import assertk.assertions.containsExactlyInAnyOrder
import assertk.assertions.havingInstancesOf
import assertk.assertions.containsNone
import assertk.assertions.containsOnly
import assertk.assertions.doesNotContain
import assertk.assertions.notHavingInstancesOf
import assertk.assertions.each
import assertk.assertions.exactly
import assertk.assertions.extracting
Expand Down Expand Up @@ -229,6 +231,32 @@ class IterableTest {
}
//endregion

//region containsInstanceOf
@Test fun containsInstanceOf_element_present_passes() {
assertThat(iterableOf(1, "two")).havingInstancesOf<String>().single().isEqualTo("two")
}

@Test fun containsInstanceOf_element_missing_fails() {
val error = assertFailsWith<AssertionError> {
assertThat(iterableOf(1, "two")).havingInstancesOf<Double>()
}
assertEquals("expected to contain at least one instance of class kotlin.Double but was [1, two]", error.message)
}
//endregion

//region doesNotContainInstanceOf
@Test fun doesNotContainInstanceOf_element_present_fails() {
val error = assertFailsWith<AssertionError>() {
assertThat(iterableOf(1, "two")).notHavingInstancesOf<String>()
}
assertEquals("expected to not contain instances of class kotlin.String but was [1, two]", error.message)
}

@Test fun doesNotContainInstanceOf_element_missing_passes() {
assertThat(iterableOf(1, "two")).notHavingInstancesOf<Double>()
}
//endregion

//region each
@Test
fun each_empty_list_passes() {
Expand Down