diff --git a/documentation/modules/ROOT/partials/release-notes/release-notes-6.2.0-M1.adoc b/documentation/modules/ROOT/partials/release-notes/release-notes-6.2.0-M1.adoc index ffa2008c1b43..934fb0eb0c7b 100644 --- a/documentation/modules/ROOT/partials/release-notes/release-notes-6.2.0-M1.adoc +++ b/documentation/modules/ROOT/partials/release-notes/release-notes-6.2.0-M1.adoc @@ -34,6 +34,10 @@ repository on GitHub. * `junit-platform-console-standalone` is now part of the `junit-bom` * `@org.junit.platform.suite.api.Disabled` can be used annotate a `@Suite`-annotated class. If so annotated, the suite will not be executed. +* The `org.junit.jupiter.api.assertThrows` Kotlin API and its related functions have + added variants that use an exception class as a value rather than a type parameter. + This is to primarily to address the current issue/s when trying to use + `@ParameterizedTest` that produces exceptions as values. [[v6.2.0-M1-junit-jupiter]] === JUnit Jupiter diff --git a/junit-jupiter-api/src/main/kotlin/org/junit/jupiter/api/Assertions.kt b/junit-jupiter-api/src/main/kotlin/org/junit/jupiter/api/Assertions.kt index 321cd7b39005..5c536aa37b2f 100644 --- a/junit-jupiter-api/src/main/kotlin/org/junit/jupiter/api/Assertions.kt +++ b/junit-jupiter-api/src/main/kotlin/org/junit/jupiter/api/Assertions.kt @@ -24,6 +24,7 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind.AT_MOST_ONCE import kotlin.contracts.InvocationKind.EXACTLY_ONCE import kotlin.contracts.contract +import kotlin.reflect.KClass /** * @see Assertions.fail @@ -285,6 +286,39 @@ inline fun assertThrows(executable: () -> Unit): T { } } +/** + * Example usage: + * ```kotlin + * val exception = assertThrows(IllegalArgumentException::class) { + * throw IllegalArgumentException("Talk to a duck") + * } + * assertEquals("Talk to a duck", exception.message) + * ``` + * + * This version of assertThrows is intended to be used in situations where reified Throwable's cannot be used, + * i.e. when using a @ParameterizedTest that provides exception classes as values. + * @see Assertions.assertThrows + */ +inline fun assertThrows( + expectedType: KClass, + executable: () -> Unit +): T { + // no contract for `executable` because it is expected to throw an exception instead + // of being executed completely (see https://youtrack.jetbrains.com/issue/KT-27748) + val throwable: Throwable? = + try { + executable() + } catch (caught: Throwable) { + caught + } as? Throwable + + return Assertions.assertThrows(expectedType.java) { + if (throwable != null) { + throw throwable + } + } +} + /** * Example usage: * ```kotlin @@ -300,6 +334,25 @@ inline fun assertThrows( executable: () -> Unit ): T = assertThrows({ message }, executable) +/** + * Example usage: + * ```kotlin + * val exception = assertThrows(IllegalArgumentException::class, "Should throw an Exception") { + * throw IllegalArgumentException("Talk to a duck") + * } + * assertEquals("Talk to a duck", exception.message) + * ``` + * + * This version of assertThrows is intended to be used in situations where reified Throwable's cannot be used, + * i.e. when using a @ParameterizedTest that provides exception classes as values. + * @see Assertions.assertThrows + */ +inline fun assertThrows( + expectedType: KClass, + message: String, + executable: () -> Unit +): T = assertThrows(expectedType, { message }, executable) + /** * Example usage: * ```kotlin @@ -339,6 +392,49 @@ inline fun assertThrows( ) } +/** + * Example usage: + * ```kotlin + * val exception = assertThrows(IllegalArgumentException::class, { "Should throw an Exception" }) { + * throw IllegalArgumentException("Talk to a duck") + * } + * assertEquals("Talk to a duck", exception.message) + * ``` + * + * This version of assertThrows is intended to be used in situations where reified Throwable's cannot be used, + * i.e. when using a @ParameterizedTest that provides exception classes as values. + * @see Assertions.assertThrows + */ +@OptIn(ExperimentalContracts::class) +inline fun assertThrows( + expectedType: KClass, + noinline message: () -> String, + executable: () -> Unit +): T { + contract { + callsInPlace(message, AT_MOST_ONCE) + // no contract for `executable` because it is expected to throw an exception instead + // of being executed completely (see https://youtrack.jetbrains.com/issue/KT-27748) + } + + val throwable: Throwable? = + try { + executable() + } catch (caught: Throwable) { + caught + } as? Throwable + + return Assertions.assertThrows( + expectedType.java, + { + if (throwable != null) { + throw throwable + } + }, + message + ) +} + /** * Example usage: * ```kotlin @@ -366,6 +462,39 @@ inline fun assertThrowsExactly(executable: () -> Unit): } } +/** + * Example usage: + * ```kotlin + * val exception = assertThrows(IllegalArgumentException::class) { + * throw IllegalArgumentException("Talk to a duck") + * } + * assertEquals("Talk to a duck", exception.message) + * ``` + * + * This version of assertThrowsExactly is intended to be used in situations where reified Throwable's cannot be used, + * i.e. when using a @ParameterizedTest that provides exception classes as values. + * @see Assertions.assertThrowsExactly + */ +inline fun assertThrowsExactly( + expectedType: KClass, + executable: () -> Unit +): T { + // no contract for `executable` because it is expected to throw an exception instead + // of being executed completely (see https://youtrack.jetbrains.com/issue/KT-27748) + val throwable: Throwable? = + try { + executable() + } catch (caught: Throwable) { + caught + } as? Throwable + + return Assertions.assertThrowsExactly(expectedType.java) { + if (throwable != null) { + throw throwable + } + } +} + /** * Example usage: * ```kotlin @@ -381,6 +510,25 @@ inline fun assertThrowsExactly( executable: () -> Unit ): T = assertThrowsExactly({ message }, executable) +/** + * Example usage: + * ```kotlin + * val exception = assertThrowsExactly(IllegalArgumentException::class, "Should throw an Exception") { + * throw IllegalArgumentException("Talk to a duck") + * } + * assertEquals("Talk to a duck", exception.message) + * ``` + * + * This version of assertThrowsExactly is intended to be used in situations where reified Throwable's cannot be used, + * i.e. when using a @ParameterizedTest that provides exception classes as values. + * @see Assertions.assertThrowsExactly + */ +inline fun assertThrowsExactly( + expectedType: KClass, + message: String, + executable: () -> Unit +): T = assertThrowsExactly(expectedType, { message }, executable) + /** * Example usage: * ```kotlin @@ -420,6 +568,49 @@ inline fun assertThrowsExactly( ) } +/** + * Example usage: + * ```kotlin + * val exception = assertThrowsExactly(IllegalArgumentException::class, { "Should throw an Exception" }) { + * throw IllegalArgumentException("Talk to a duck") + * } + * assertEquals("Talk to a duck", exception.message) + * ``` + * + * This version of assertThrowsExactly is intended to be used in situations where reified Throwable's cannot be used, + * i.e. when using a @ParameterizedTest that provides exception classes as values. + * @see Assertions.assertThrowsExactly + */ +@OptIn(ExperimentalContracts::class) +inline fun assertThrowsExactly( + expectedType: KClass, + noinline message: () -> String, + executable: () -> Unit +): T { + contract { + callsInPlace(message, AT_MOST_ONCE) + // no contract for `executable` because it is expected to throw an exception instead + // of being executed completely (see https://youtrack.jetbrains.com/issue/KT-27748) + } + + val throwable: Throwable? = + try { + executable() + } catch (caught: Throwable) { + caught + } as? Throwable + + return Assertions.assertThrowsExactly( + expectedType.java, + { + if (throwable != null) { + throw throwable + } + }, + message + ) +} + /** * Example usage: * ```kotlin diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/kotlin/KotlinAssertionsTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/kotlin/KotlinAssertionsTests.kt index 8e271bd4b08d..6e974b8f96b0 100644 --- a/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/kotlin/KotlinAssertionsTests.kt +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/kotlin/KotlinAssertionsTests.kt @@ -71,6 +71,13 @@ class KotlinAssertionsTests { assertThrows("should fail") { fail({ "message" }) } assertThrows({ "should fail" }) { fail(AssertionError()) } assertThrows({ "should fail" }) { fail(null as Throwable?) } + + assertThrows(AssertionError::class) { fail("message") } + assertThrows(AssertionError::class) { fail("message", AssertionError()) } + assertThrows(AssertionError::class) { fail("message", null) } + assertThrows(AssertionError::class, "should fail") { fail({ "message" }) } + assertThrows(AssertionError::class, { "should fail" }) { fail(AssertionError()) } + assertThrows(AssertionError::class, { "should fail" }) { fail(null as Throwable?) } } @Test @@ -78,16 +85,27 @@ class KotlinAssertionsTests { assertThrowsExactly { fail("message") } assertThrowsExactly("should fail") { fail("message") } assertThrowsExactly({ "should fail" }) { fail("message") } + + assertThrowsExactly(AssertionFailedError::class) { fail("message") } + assertThrowsExactly(AssertionFailedError::class, "should fail") { fail("message") } + assertThrowsExactly(AssertionFailedError::class, { "should fail" }) { fail("message") } } @Test - fun `expected context exception testing`() = + fun `expected context exception testing`() { runBlocking { assertThrows("Should fail async") { suspend { fail("Should fail async") }() } } + runBlocking { + assertThrows(AssertionError::class, "Should fail async") { + suspend { fail("Should fail async") }() + } + } + } + @TestFactory fun `assertDoesNotThrow behaves as expected`(): Stream = Stream.of( @@ -141,6 +159,18 @@ class KotlinAssertionsTests { "Unexpected exception thrown: org.opentest4j.AssertionFailedError: fail" ) }, + dynamicTest("for no arguments variant (exception as value") { + val exception = + assertThrows(AssertionError::class) { + assertDoesNotThrow { + fail("fail") + } + } + assertMessageEquals( + exception, + "Unexpected exception thrown: org.opentest4j.AssertionFailedError: fail" + ) + }, dynamicTest("for no arguments variant (suspended)") { runBlocking { val exception = @@ -155,6 +185,20 @@ class KotlinAssertionsTests { ) } }, + dynamicTest("for no arguments variant (suspended, exception as value)") { + runBlocking { + val exception = + assertThrows(AssertionError::class) { + assertDoesNotThrow { + suspend { fail("fail") }() + } + } + assertMessageEquals( + exception, + "Unexpected exception thrown: org.opentest4j.AssertionFailedError: fail" + ) + } + }, dynamicTest("for message variant") { val exception = assertThrows { @@ -167,6 +211,18 @@ class KotlinAssertionsTests { "Does not throw ==> Unexpected exception thrown: org.opentest4j.AssertionFailedError: fail" ) }, + dynamicTest("for message variant (exception as value)") { + val exception = + assertThrows(AssertionError::class) { + assertDoesNotThrow("Does not throw") { + fail("fail") + } + } + assertMessageEquals( + exception, + "Does not throw ==> Unexpected exception thrown: org.opentest4j.AssertionFailedError: fail" + ) + }, dynamicTest("for message variant (suspended)") { runBlocking { val exception = @@ -181,6 +237,20 @@ class KotlinAssertionsTests { ) } }, + dynamicTest("for message variant (suspended, exception as value)") { + runBlocking { + val exception = + assertThrows(AssertionError::class) { + assertDoesNotThrow("Does not throw") { + suspend { fail("fail") }() + } + } + assertMessageEquals( + exception, + "Does not throw ==> Unexpected exception thrown: org.opentest4j.AssertionFailedError: fail" + ) + } + }, dynamicTest("for message supplier variant") { val exception = assertThrows { @@ -193,6 +263,18 @@ class KotlinAssertionsTests { "Does not throw ==> Unexpected exception thrown: org.opentest4j.AssertionFailedError: fail" ) }, + dynamicTest("for message supplier variant (exception as value)") { + val exception = + assertThrows(AssertionError::class) { + assertDoesNotThrow({ "Does not throw" }) { + fail("fail") + } + } + assertMessageEquals( + exception, + "Does not throw ==> Unexpected exception thrown: org.opentest4j.AssertionFailedError: fail" + ) + }, dynamicTest("for message supplier variant (suspended)") { runBlocking { val exception = @@ -206,6 +288,20 @@ class KotlinAssertionsTests { "Does not throw ==> Unexpected exception thrown: org.opentest4j.AssertionFailedError: fail" ) } + }, + dynamicTest("for message supplier variant (suspended, exception as value)") { + runBlocking { + val exception = + assertThrows(AssertionError::class) { + assertDoesNotThrow({ "Does not throw" }) { + suspend { fail("fail") }() + } + } + assertMessageEquals( + exception, + "Does not throw ==> Unexpected exception thrown: org.opentest4j.AssertionFailedError: fail" + ) + } } ) ) @@ -217,7 +313,14 @@ class KotlinAssertionsTests { assertThrows("Should have thrown multiple errors") { assertAll(Stream.of({ assertFalse(true) }, { assertFalse(true) })) } + + val multipleFailuresErrorExceptionAsValue = + assertThrows(MultipleFailuresError::class, "Should have thrown multiple errors") { + assertAll(Stream.of({ assertFalse(true) }, { assertFalse(true) })) + } + assertExpectedExceptionTypes(multipleFailuresError, AssertionFailedError::class, AssertionFailedError::class) + assertExpectedExceptionTypes(multipleFailuresErrorExceptionAsValue, AssertionFailedError::class, AssertionFailedError::class) } @Test @@ -226,7 +329,13 @@ class KotlinAssertionsTests { assertThrows("Should have thrown multiple errors") { assertAll(setOf({ assertFalse(true) }, { assertFalse(true) })) } + + val multipleFailuresErrorExceptionAsValue = + assertThrows(MultipleFailuresError::class, "Should have thrown multiple errors") { + assertAll(setOf({ assertFalse(true) }, { assertFalse(true) })) + } assertExpectedExceptionTypes(multipleFailuresError, AssertionFailedError::class, AssertionFailedError::class) + assertExpectedExceptionTypes(multipleFailuresErrorExceptionAsValue, AssertionFailedError::class, AssertionFailedError::class) } @Test @@ -238,7 +347,15 @@ class KotlinAssertionsTests { // This should never execute: expectAssertionFailedError() } + + val errorExceptionAsValue = + assertThrows("assertThrows did not throw the correct exception") { + assertThrows(IllegalStateException::class, assertionMessage) { } + // This should never execute: + expectAssertionFailedError() + } assertMessageStartsWith(error, assertionMessage) + assertMessageStartsWith(errorExceptionAsValue, assertionMessage) } @Test @@ -254,7 +371,12 @@ class KotlinAssertionsTests { assertThrows { assertInstanceOf(StringBuilder(), "Should be a String") } + val resultExceptionAsValue = + assertThrows(AssertionError::class) { + assertInstanceOf(StringBuilder(), "Should be a String") + } assertMessageStartsWith(result, "Should be a String") + assertMessageStartsWith(resultExceptionAsValue, "Should be a String") } @Test @@ -263,7 +385,12 @@ class KotlinAssertionsTests { assertThrows { assertInstanceOf(null, "Should be a String") } + val resultExceptionAsValue = + assertThrows(AssertionError::class) { + assertInstanceOf(null, "Should be a String") + } assertMessageStartsWith(result, "Should be a String") + assertMessageStartsWith(resultExceptionAsValue, "Should be a String") } @Test @@ -288,8 +415,13 @@ class KotlinAssertionsTests { assertThrows { assertInstanceOf(null) } + val errorExceptionAsValue = + assertThrows(AssertionFailedError::class) { + assertInstanceOf(null) + } assertMessageStartsWith(error, "Unexpected null value") + assertMessageStartsWith(errorExceptionAsValue, "Unexpected null value") } @Test