diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.3.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.3.0-M1.adoc index 31e739191403..8de4ffac51d1 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.3.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.3.0-M1.adoc @@ -39,7 +39,9 @@ on GitHub. ==== New Features and Improvements -* ❓ +* New `assertThrows` methods in `Assertions` provide a more specific failure + message if the lambda returns a result instead of throwing the expected + exception. [[release-notes-5.3.0-M1-junit-vintage]] diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertThrows.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertThrows.java index a90cb3096844..9e716dca5a0a 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertThrows.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertThrows.java @@ -18,6 +18,7 @@ import java.util.function.Supplier; import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.api.function.ThrowingSupplier; import org.opentest4j.AssertionFailedError; /** @@ -34,25 +35,45 @@ private AssertThrows() { } ///CLOVER:ON + static T assertThrows(Class expectedType, ThrowingSupplier supplier) { + return assertThrows(expectedType, supplier::get, (Object) null); + } + static T assertThrows(Class expectedType, Executable executable) { - return assertThrows(expectedType, executable, (Object) null); + return assertThrows(expectedType, asSupplier(executable), (Object) null); + } + + static T assertThrows(Class expectedType, ThrowingSupplier supplier, String message) { + return assertThrows(expectedType, supplier::get, (Object) message); } static T assertThrows(Class expectedType, Executable executable, String message) { - return assertThrows(expectedType, executable, (Object) message); + return assertThrows(expectedType, asSupplier(executable), (Object) message); + } + + static T assertThrows(Class expectedType, ThrowingSupplier supplier, + Supplier messageSupplier) { + return assertThrows(expectedType, supplier::get, (Object) messageSupplier); } static T assertThrows(Class expectedType, Executable executable, Supplier messageSupplier) { - return assertThrows(expectedType, executable, (Object) messageSupplier); + return assertThrows(expectedType, asSupplier(executable), (Object) messageSupplier); } @SuppressWarnings("unchecked") private static T assertThrows(Class expectedType, Executable executable, Object messageOrSupplier) { + return assertThrows(expectedType, asSupplier(executable), messageOrSupplier); + } + @SuppressWarnings("unchecked") + private static T assertThrows(Class expectedType, ThrowingResultSupplier supplier, + Object messageOrSupplier) { + + Object result; try { - executable.execute(); + result = supplier.get(); } catch (Throwable actualException) { if (expectedType.isInstance(actualException)) { @@ -66,8 +87,34 @@ private static T assertThrows(Class expectedType, Execu } String message = buildPrefix(nullSafeGet(messageOrSupplier)) - + String.format("Expected %s to be thrown, but nothing was thrown.", getCanonicalName(expectedType)); + + String.format("Expected %s to be thrown, but nothing was thrown", getCanonicalName(expectedType)) + + (supplier.formatResult() ? String.format(" (returned %s).", result) : "."); throw new AssertionFailedError(message); } + private interface ThrowingResultSupplier extends ThrowingSupplier { + /** + * Returns true if the result should be included in the failure message in the case where the supplier + * returns a result instead of throwing the expected exception. + */ + default boolean formatResult() { + return true; + } + } + + private static ThrowingResultSupplier asSupplier(Executable executable) { + return new ThrowingResultSupplier() { + @Override + public Void get() throws Throwable { + executable.execute(); + return null; + } + + @Override + public boolean formatResult() { + return false; + } + }; + } + } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java index ef1901e2d5a8..b21832b938b8 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java @@ -1169,6 +1169,23 @@ public static T assertThrows(Class expectedType, Execut return AssertThrows.assertThrows(expectedType, executable); } + /** + * Asserts that execution of the supplied {@code executable} throws + * an exception of the {@code expectedType} and returns the exception. + * + *

If no exception is thrown, or if an exception of a different type is + * thrown, this method will fail. + * + *

If you do not want to perform additional checks on the exception instance, + * simply ignore the return value. + * + *

If the given {@link ThrowingSupplier} returns a result instead of throwing the expected exception, + * the result will be included in the failure message. + */ + public static T assertThrows(Class expectedType, ThrowingSupplier supplier) { + return AssertThrows.assertThrows(expectedType, supplier); + } + /** * Asserts that execution of the supplied {@code executable} throws * an exception of the {@code expectedType} and returns the exception. @@ -1183,6 +1200,24 @@ public static T assertThrows(Class expectedType, Execut return AssertThrows.assertThrows(expectedType, executable, message); } + /** + * Asserts that execution of the supplied {@code executable} throws + * an exception of the {@code expectedType} and returns the exception. + * + *

If no exception is thrown, or if an exception of a different type is + * thrown, this method will fail. + * + *

If you do not want to perform additional checks on the exception instance, + * simply ignore the return value. + * + *

If the given {@link ThrowingSupplier} returns a result instead of throwing the expected exception, + * the result will be included in the failure message. + */ + public static T assertThrows(Class expectedType, ThrowingSupplier supplier, + String message) { + return AssertThrows.assertThrows(expectedType, supplier, message); + } + /** * Asserts that execution of the supplied {@code executable} throws * an exception of the {@code expectedType} and returns the exception. @@ -1201,6 +1236,27 @@ public static T assertThrows(Class expectedType, Execut return AssertThrows.assertThrows(expectedType, executable, messageSupplier); } + /** + * Asserts that execution of the supplied {@code executable} throws + * an exception of the {@code expectedType} and returns the exception. + * + *

If no exception is thrown, or if an exception of a different type is + * thrown, this method will fail. + * + *

If necessary, the failure message will be retrieved lazily from the + * supplied {@code messageSupplier}. + * + *

If you do not want to perform additional checks on the exception instance, + * simply ignore the return value. + * + *

If the given {@link ThrowingSupplier} returns a result instead of throwing the expected exception, + * the result will be included in the failure message. + */ + public static T assertThrows(Class expectedType, ThrowingSupplier supplier, + Supplier messageSupplier) { + return AssertThrows.assertThrows(expectedType, supplier, messageSupplier); + } + // --- executable --- /** diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertThrowsAssertionsTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertThrowsAssertionsTests.java index 5bc0adbfb80c..68f61eb76869 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertThrowsAssertionsTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertThrowsAssertionsTests.java @@ -11,6 +11,7 @@ package org.junit.jupiter.api; import static org.junit.jupiter.api.AssertionTestUtils.assertMessageContains; +import static org.junit.jupiter.api.AssertionTestUtils.assertMessageDoesNotContain; import static org.junit.jupiter.api.AssertionTestUtils.assertMessageEquals; import static org.junit.jupiter.api.AssertionTestUtils.assertMessageStartsWith; import static org.junit.jupiter.api.AssertionTestUtils.expectAssertionFailedError; @@ -235,6 +236,64 @@ void assertThrowsWithExecutableThatThrowsSameExceptionTypeFromDifferentClassLoad } } + @Test + void assertThrowsReturns() { + try { + assertThrows(EnigmaThrowable.class, () -> 42); + expectAssertionFailedError(); + } + catch (AssertionFailedError ex) { + assertMessageContains(ex, "(returned 42)"); + } + } + + @Test + void assertThrowsReturnsNull() { + try { + assertThrows(EnigmaThrowable.class, () -> null); + expectAssertionFailedError(); + } + catch (AssertionFailedError ex) { + assertMessageContains(ex, "(returned null)"); + } + } + + @Test + void assertThrowsReturnsVoid() { + try { + assertThrows(EnigmaThrowable.class, () -> { + }); + expectAssertionFailedError(); + } + catch (AssertionFailedError ex) { + assertMessageDoesNotContain(ex, "(returned null)"); + } + } + + @Test + void assertThrowsReturnsCustomMessage() { + try { + assertThrows(EnigmaThrowable.class, () -> 42, "custom message"); + expectAssertionFailedError(); + } + catch (AssertionFailedError ex) { + assertMessageContains(ex, "(returned 42)"); + assertMessageContains(ex, "custom message"); + } + } + + @Test + void assertThrowsReturnsCustomMessageSupplier() { + try { + assertThrows(EnigmaThrowable.class, () -> 42, () -> "custom message"); + expectAssertionFailedError(); + } + catch (AssertionFailedError ex) { + assertMessageContains(ex, "(returned 42)"); + assertMessageContains(ex, "custom message"); + } + } + @SuppressWarnings("serial") private static class LocalException extends RuntimeException { } diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertionTestUtils.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertionTestUtils.java index bb030adbd199..c2190f6f53d4 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertionTestUtils.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertionTestUtils.java @@ -61,6 +61,13 @@ static void assertMessageContains(Throwable ex, String msg) throws AssertionErro } } + static void assertMessageDoesNotContain(Throwable ex, String msg) throws AssertionError { + if (ex.getMessage().contains(msg)) { + throw new AssertionError( + "Exception message should contain [" + msg + "], but was [" + ex.getMessage() + "]."); + } + } + static void assertExpectedAndActualValues(AssertionFailedError ex, Object expected, Object actual) throws AssertionError { if (!wrapsEqualValue(ex.getExpected(), expected)) {