Repository: willowtreeapps/assertk (kotlin)

Issue:
Version of `IsIn`/`isNotIn` that takes an `Iterable`?
I have my expected values in a constant that's shared, but I have to do `isIn(*values)` with an `arrayOf` constant instead of `isIn(values)` with a `listOf` constant today.

Pull request description:
This adds some convenient methods when asserting something is (and is not) in something else. Previously due to `isIn` and `isNotIn` only accepting `vararg values: T`, this caused a somewhat confusing error.
Before:
```kotlin
@Test
fun demo() {
    val possibleValues = listOf("test")
    assertThat("test").isIn(possibleValues) // expected:<[["test"]]> to contain:<"test">
}
```
As mentioned in #549 to achieve something similar to above, you would need to spread an `Array<T>`
```kotlin
@Test
fun demo() {
    val possibleValues = arrayOf("test")
    assertThat("test").isIn(*possibleValues) // does not error
}
```

Now with `isIn(Iterable<T>)` and `isNotIn(Iterable<T>)`
```kotlin
@Test
fun demo() {
    val possibleValues = listOf("test")
    assertThat("test").isIn(possibleValues) // does not error
}
```
does not error.

fixes #549

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a0abb61..52e9ebe2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
 - renamed `suspendCall` to `having` as part of effort to unify API naming, old name is deprecated.
 - added `doesNotExist` assertions to `Path`.
 - the receiver types for `isTrue()`, `isFalse()`, and `isInstanceOf()` have been widened to operate on nullable values.
+- added assertions `isIn(Iterable<T>)` and `isNotIn(Iterable<T>)`
 
 ## [0.28.1] 2024-04-17
 
diff --git a/assertk/src/commonMain/kotlin/assertk/assertions/any.kt b/assertk/src/commonMain/kotlin/assertk/assertions/any.kt
index fbc37695..edb442ac 100644
--- a/assertk/src/commonMain/kotlin/assertk/assertions/any.kt
+++ b/assertk/src/commonMain/kotlin/assertk/assertions/any.kt
@@ -97,6 +97,15 @@ fun <T> Assert<T>.isIn(vararg values: T) = given { actual ->
     expected(":${show(values)} to contain:${show(actual)}")
 }
 
+/**
+ * Asserts the value is in the expected values, using `in`.
+ * @see [isNotIn]
+ */
+fun <T> Assert<T>.isIn(values: Iterable<T>) = given { actual ->
+    if (actual in values) return
+    expected(":${show(values)} to contain:${show(actual)}")
+}
+
 /**
  * Asserts the value is not in the expected values, using `!in`.
  * @see [isIn]
@@ -106,6 +115,15 @@ fun <T> Assert<T>.isNotIn(vararg values: T) = given { actual ->
     expected(":${show(values)} to not contain:${show(actual)}")
 }
 
+/**
+ * Asserts the value is not in the expected values, using `!in`.
+ * @see [isIn]
+ */
+fun <T> Assert<T>.isNotIn(values: Iterable<T>) = given { actual ->
+    if (actual !in values) return
+    expected(":${show(values)} to not contain:${show(actual)}")
+}
+
 /**
  * Asserts the value has the expected string from it's [toString].
  */


Test patch:
diff --git a/assertk/src/commonTest/kotlin/test/assertk/assertions/AnyTest.kt b/assertk/src/commonTest/kotlin/test/assertk/assertions/AnyTest.kt
index a069382d..4820bbdb 100644
--- a/assertk/src/commonTest/kotlin/test/assertk/assertions/AnyTest.kt
+++ b/assertk/src/commonTest/kotlin/test/assertk/assertions/AnyTest.kt
@@ -153,6 +153,36 @@ class AnyTest {
         assertEquals("expected:<[not test1, not test2]> to contain:<test>", error.message)
     }
 
+    @Test
+    fun isIn_iterable_one_equal_item_passes() {
+        val list = listOf(BasicObject("test"))
+        assertThat(subject).isIn(list)
+    }
+
+    @Test
+    fun isIn_iterable_one_non_equal_item_fails() {
+        val list = listOf(BasicObject("not test1"))
+        val error = assertFailsWith<AssertionError> {
+            assertThat(subject).isIn(list)
+        }
+        assertEquals("expected:<[not test1]> to contain:<test>", error.message)
+    }
+
+    @Test
+    fun isIn_iterable_one_equal_item_in_may_passes() {
+        val list = listOf(BasicObject("not test1"), BasicObject("test"), BasicObject("not test2"))
+        assertThat(subject).isIn(list)
+    }
+
+    @Test
+    fun isIn_iterable_no_equal_items_in_may_fails() {
+        val list = listOf(BasicObject("not test1"), BasicObject("not test2"))
+        val error = assertFailsWith<AssertionError> {
+            assertThat(subject).isIn(list)
+        }
+        assertEquals("expected:<[not test1, not test2]> to contain:<test>", error.message)
+    }
+
     @Test
     fun isNotIn_one_non_equal_item_passes() {
         val isOut1 = BasicObject("not test1")
@@ -186,6 +216,36 @@ class AnyTest {
         assertEquals("expected:<[not test1, test, not test2]> to not contain:<test>", error.message)
     }
 
+    @Test
+    fun isNotIn_iterable_one_non_equal_item_passes() {
+        val list = listOf(BasicObject("not test1"))
+        assertThat(subject).isNotIn(list)
+    }
+
+    @Test
+    fun isNotIn_iterable_one_equal_item_fails() {
+        val list = listOf(BasicObject("test"))
+        val error = assertFailsWith<AssertionError> {
+            assertThat(subject).isNotIn(list)
+        }
+        assertEquals("expected:<[test]> to not contain:<test>", error.message)
+    }
+
+    @Test
+    fun isNotIn_iterable_no_equal_items_in_many_passes() {
+        val list = listOf(BasicObject("not test1"), BasicObject("not test2"))
+        assertThat(subject).isNotIn(list)
+    }
+
+    @Test
+    fun isNotIn_iterable_one_equal_item_in_many_fails() {
+        val list = listOf(BasicObject("not test1"), BasicObject("test"), BasicObject("not test2"))
+        val error = assertFailsWith<AssertionError> {
+            assertThat(subject).isNotIn(list)
+        }
+        assertEquals("expected:<[not test1, test, not test2]> to not contain:<test>", error.message)
+    }
+
     @Test
     fun hasToString_equal_string_passes() {
         assertThat(subject).hasToString("test")