Repository: willowtreeapps/assertk
Pull request #554 (2024-10-28 19:00:19)

Widen `isInstanceOf` assert to operate on nullable types
This behavior brings it more line with how 'is' behaves in the language.

Refs #393. Refs #550.

JakeWharton: > Do we also want `isNotInstanceOf` to take null for the same reason?

Done! I'm not sure how useful this is in practice, but consistency with the positive assertion and the language makes sense.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 827ddef..6a0abb6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
 - renamed `prop` to `having` as part of effort to unify API naming, old name is deprecated.
 - 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.
 
 ## [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 6ad0410..fbc3769 100644
--- a/assertk/src/commonMain/kotlin/assertk/assertions/any.kt
+++ b/assertk/src/commonMain/kotlin/assertk/assertions/any.kt
@@ -251,7 +251,7 @@ fun <T : Any> Assert<T>.doesNotHaveClass(kclass: KClass<out T>) = given { actual
  * @see [isInstanceOf]
  * @see [doesNotHaveClass]
  */
-inline fun <reified T : Any> Assert<Any>.isNotInstanceOf() = isNotInstanceOf(T::class)
+inline fun <reified T : Any> Assert<Any?>.isNotInstanceOf() = isNotInstanceOf(T::class)
 
 /**
  * Asserts the value is not an instance of the expected kotlin class. Both
@@ -259,7 +259,7 @@ inline fun <reified T : Any> Assert<Any>.isNotInstanceOf() = isNotInstanceOf(T::
  * @see [isInstanceOf]
  * @see [doesNotHaveClass]
  */
-fun <T : Any> Assert<T>.isNotInstanceOf(kclass: KClass<out T>) = given { actual ->
+fun <T : Any> Assert<T?>.isNotInstanceOf(kclass: KClass<out T>) = given { actual ->
     if (!kclass.isInstance(actual)) return
     expected("to not be instance of:${show(kclass)}")
 }
@@ -270,7 +270,7 @@ fun <T : Any> Assert<T>.isNotInstanceOf(kclass: KClass<out T>) = given { actual
  * @see [isNotInstanceOf]
  * @see [hasClass]
  */
-inline fun <reified T : Any> Assert<Any>.isInstanceOf() = isInstanceOf(T::class)
+inline fun <reified T : Any> Assert<Any?>.isInstanceOf() = isInstanceOf(T::class)
 
 /**
  * Asserts the value is an instance of the expected kotlin class. Both `assertThat("test").isInstanceOf(String::class)` and
@@ -278,12 +278,13 @@ inline fun <reified T : Any> Assert<Any>.isInstanceOf() = isInstanceOf(T::class)
  * @see [isNotInstanceOf]
  * @see [hasClass]
  */
-fun <T : Any> Assert<Any>.isInstanceOf(kclass: KClass<T>): Assert<T> = transform(name) { actual ->
+fun <T : Any> Assert<Any?>.isInstanceOf(kclass: KClass<T>): Assert<T> = transform(name) { actual ->
     if (kclass.isInstance(actual)) {
         @Suppress("UNCHECKED_CAST")
         actual as T
     } else {
-        expected("to be instance of:${show(kclass)} but had class:${show(actual::class)}")
+        val but = if (actual == null) "was null" else "had class:${show(actual::class)}"
+        expected("to be instance of:${show(kclass)} but $but")
     }
 }
 
diff --git a/assertk/src/commonMain/kotlin/assertk/assertions/boolean.kt b/assertk/src/commonMain/kotlin/assertk/assertions/boolean.kt
index 9054576..75812ad 100644
--- a/assertk/src/commonMain/kotlin/assertk/assertions/boolean.kt
+++ b/assertk/src/commonMain/kotlin/assertk/assertions/boolean.kt
@@ -2,7 +2,6 @@ package assertk.assertions
 
 import assertk.Assert
 import assertk.assertions.support.expected
-import assertk.assertions.support.fail
 
 /**
  * Asserts the boolean is true.
@@ -10,7 +9,7 @@ import assertk.assertions.support.fail
  */
 fun Assert<Boolean?>.isTrue() = given { actual ->
     if (actual == true) return
-    fail(true, actual)
+    expected("to be true but was $actual")
 }
 
 /**
@@ -19,5 +18,5 @@ fun Assert<Boolean?>.isTrue() = given { actual ->
  */
 fun Assert<Boolean?>.isFalse() = given { actual ->
     if (actual == false) return
-    fail(false, actual)
+    expected("to be false but was $actual")
 }
diff --git a/assertk/src/jvmMain/kotlin/assertk/assertions/any.kt b/assertk/src/jvmMain/kotlin/assertk/assertions/any.kt
index c9a6f74..d995175 100644
--- a/assertk/src/jvmMain/kotlin/assertk/assertions/any.kt
+++ b/assertk/src/jvmMain/kotlin/assertk/assertions/any.kt
@@ -46,12 +46,13 @@ fun <T : Any> Assert<T>.doesNotHaveClass(jclass: Class<out T>) = given { actual
  * @see [isNotInstanceOf]
  * @see [hasClass]
  */
-fun <T : Any, S : T> Assert<T>.isInstanceOf(jclass: Class<S>): Assert<S> = transform { actual ->
+fun <T : Any> Assert<Any?>.isInstanceOf(jclass: Class<T>): Assert<T> = transform { actual ->
     if (jclass.isInstance(actual)) {
         @Suppress("UNCHECKED_CAST")
-        actual as S
+        actual as T
     } else {
-        expected("to be instance of:${show(jclass)} but had class:${show(actual.javaClass)}")
+        val but = if (actual == null) "was null" else "had class:${show(actual.javaClass)}"
+        expected("to be instance of:${show(jclass)} but $but")
     }
 }
 
@@ -61,7 +62,7 @@ fun <T : Any, S : T> Assert<T>.isInstanceOf(jclass: Class<S>): Assert<S> = trans
  * @see [isInstanceOf]
  * @see [doesNotHaveClass]
  */
-fun <T : Any> Assert<T>.isNotInstanceOf(jclass: Class<out T>) = given { actual ->
+fun <T : Any> Assert<T?>.isNotInstanceOf(jclass: Class<out T>) = given { actual ->
     if (!jclass.isInstance(actual)) return
     expected("to not be instance of:${show(jclass)}")
 }


Test patch:
diff --git a/assertk/src/commonTest/kotlin/test/assertk/TableTest.kt b/assertk/src/commonTest/kotlin/test/assertk/TableTest.kt
index c93e8aa..066d21f 100644
--- a/assertk/src/commonTest/kotlin/test/assertk/TableTest.kt
+++ b/assertk/src/commonTest/kotlin/test/assertk/TableTest.kt
@@ -88,7 +88,7 @@ class TableTest {
         assertEquals(
             """The following assertion failed
 	          |${"\t"}on row:(a=<false>)
-	          |${"\t"}expected:<[tru]e> but was:<[fals]e>
+	          |${"\t"}expected to be true but was false
             """.trimMargin(),
             error.message
         )
diff --git a/assertk/src/commonTest/kotlin/test/assertk/assertions/AnyTest.kt b/assertk/src/commonTest/kotlin/test/assertk/assertions/AnyTest.kt
index 17c9fa5..a069382 100644
--- a/assertk/src/commonTest/kotlin/test/assertk/assertions/AnyTest.kt
+++ b/assertk/src/commonTest/kotlin/test/assertk/assertions/AnyTest.kt
@@ -453,6 +453,28 @@ class AnyTest {
         )
     }
 
+    @Test
+    fun isInstanceOf_kclass_null_fails() {
+        val error = assertFailsWith<AssertionError> {
+            assertThat(null as DifferentObject?).isInstanceOf(DifferentObject::class)
+        }
+        assertEquals(
+            "expected to be instance of:<${DifferentObject::class}> but was null",
+            error.message
+        )
+    }
+
+    @Test
+    fun isInstanceOf_reified_kclass_null_fails() {
+        val error = assertFailsWith<AssertionError> {
+            assertThat(null as DifferentObject?).isInstanceOf<DifferentObject>()
+        }
+        assertEquals(
+            "expected to be instance of:<${DifferentObject::class}> but was null",
+            error.message
+        )
+    }
+
     @Test
     fun isInstanceOf_kclass_run_block_when_passes() {
         val error = assertFailsWith<AssertionError> {
@@ -503,6 +525,16 @@ class AnyTest {
         assertThat(subject).isNotInstanceOf<DifferentObject>()
     }
 
+    @Test
+    fun isNotInstanceOf_kclass_null_passes() {
+        assertThat(null as DifferentObject?).isNotInstanceOf(DifferentObject::class)
+    }
+
+    @Test
+    fun isNotInstanceOf_reified_kclass_null_passes() {
+        assertThat(null as DifferentObject?).isNotInstanceOf<DifferentObject>()
+    }
+
     @Test
     fun isNotInstanceOf_kclass_same_class_fails() {
         val error = assertFailsWith<AssertionError> {
diff --git a/assertk/src/commonTest/kotlin/test/assertk/assertions/BooleanTest.kt b/assertk/src/commonTest/kotlin/test/assertk/assertions/BooleanTest.kt
index 50ccf47..bb6611b 100644
--- a/assertk/src/commonTest/kotlin/test/assertk/assertions/BooleanTest.kt
+++ b/assertk/src/commonTest/kotlin/test/assertk/assertions/BooleanTest.kt
@@ -19,7 +19,7 @@ class BooleanTest {
         val error = assertFailsWith<AssertionError> {
             assertThat(false).isTrue()
         }
-        assertEquals("expected:<[tru]e> but was:<[fals]e>", error.message)
+        assertEquals("expected to be true but was false", error.message)
     }
 
     @Test
@@ -27,7 +27,7 @@ class BooleanTest {
         val error = assertFailsWith<AssertionError> {
             assertThat(null as Boolean?).isTrue()
         }
-        assertEquals("expected:<true> but was:<null>", error.message)
+        assertEquals("expected to be true but was null", error.message)
     }
     //endregion
 
@@ -42,7 +42,7 @@ class BooleanTest {
         val error = assertFailsWith<AssertionError> {
             assertThat(true).isFalse()
         }
-        assertEquals("expected:<[fals]e> but was:<[tru]e>", error.message)
+        assertEquals("expected to be false but was true", error.message)
     }
 
     @Test
@@ -50,7 +50,7 @@ class BooleanTest {
         val error = assertFailsWith<AssertionError> {
             assertThat(null as Boolean?).isFalse()
         }
-        assertEquals("expected:<false> but was:<null>", error.message)
+        assertEquals("expected to be false but was null", error.message)
     }
     //endregion
 }
diff --git a/assertk/src/jvmTest/kotlin/test/assertk/assertions/JavaAnyTest.kt b/assertk/src/jvmTest/kotlin/test/assertk/assertions/JavaAnyTest.kt
index ed81e3a..945994a 100644
--- a/assertk/src/jvmTest/kotlin/test/assertk/assertions/JavaAnyTest.kt
+++ b/assertk/src/jvmTest/kotlin/test/assertk/assertions/JavaAnyTest.kt
@@ -3,7 +3,6 @@ package test.assertk.assertions
 import assertk.assertThat
 import assertk.assertions.*
 import test.assertk.opentestPackageName
-import java.lang.Exception
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
@@ -30,6 +29,17 @@ class JavaAnyTest {
         assertThat(subject).isInstanceOf(TestObject::class.java)
     }
 
+    @Test
+    fun isInstanceOf_jclass_null_fails() {
+        val error = assertFailsWith<AssertionError> {
+            assertThat(null as BasicObject?).isInstanceOf(BasicObject::class.java)
+        }
+        assertEquals(
+            "expected to be instance of:<$p\$BasicObject> but was null",
+            error.message,
+        )
+    }
+
     @Test
     fun isInstanceOf_jclass_different_class_fails() {
         val error = assertFailsWith<AssertionError> {
@@ -72,6 +82,11 @@ class JavaAnyTest {
         assertThat(subject).isNotInstanceOf(DifferentObject::class.java)
     }
 
+    @Test
+    fun isNotInstanceOf_jclass_null_passess() {
+        assertThat(null as DifferentObject?).isNotInstanceOf(DifferentObject::class.java)
+    }
+
     @Test
     fun isNotInstanceOf_jclass_same_class_fails() {
         val error = assertFailsWith<AssertionError> {