diff --git a/src/main/java/org/junit/Assert.java b/src/main/java/org/junit/Assert.java index a3d7905e6d24..f924fa66d551 100644 --- a/src/main/java/org/junit/Assert.java +++ b/src/main/java/org/junit/Assert.java @@ -3,6 +3,7 @@ import org.hamcrest.Matcher; import org.hamcrest.MatcherAssert; import org.junit.function.ThrowingRunnable; +import org.junit.function.ThrowingSupplier; import org.junit.internal.ArrayComparisonFailure; import org.junit.internal.ExactComparisonCriteria; import org.junit.internal.InexactComparisonCriteria; @@ -983,6 +984,24 @@ public static T assertThrows(Class expectedThrowable, return assertThrows(null, expectedThrowable, runnable); } + /** + * Asserts that {@code supplier} throws an exception of type {@code expectedThrowable} when + * executed. If it does, the exception object is returned. If the given {@code supplier} returns + * a result instead of throwing an exception, the result will be included in the failure message. + * If it throws the wrong type of exception, an {@code AssertionError} is thrown describing the + * mismatch; the exception that was actually thrown can be obtained by calling + * {@link AssertionError#getCause}. + * + * @param expectedThrowable the expected type of the exception + * @param supplier a function that is expected to throw an exception when executed + * @return the exception thrown by {@code supplier} + * @since 4.13 + */ + public static T assertThrows(Class expectedThrowable, + ThrowingSupplier supplier) { + return assertThrows(null, expectedThrowable, supplier); + } + /** * Asserts that {@code runnable} throws an exception of type {@code expectedThrowable} when * executed. If it does, the exception object is returned. If it does not throw an exception, an @@ -999,8 +1018,29 @@ public static T assertThrows(Class expectedThrowable, */ public static T assertThrows(String message, Class expectedThrowable, ThrowingRunnable runnable) { + return assertThrows(message, expectedThrowable, new ThrowingSupplierAdapter(runnable)); + } + + /** + * Asserts that {@code supplier} throws an exception of type {@code expectedThrowable} when + * executed. If it does, the exception object is returned. If the given {@code supplier} returns + * a result instead of throwing an exception, the result will be included in the failure message. + * If it throws the wrong type of exception, an {@code AssertionError} is thrown describing the + * mismatch; the exception that was actually thrown can be obtained by calling + * {@link AssertionError#getCause}. + * + * @param message the identifying message for the {@link AssertionError} (null + * okay) + * @param expectedThrowable the expected type of the exception + * @param supplier a function that is expected to throw an exception when executed + * @return the exception thrown by {@code supplier} + * @since 4.13 + */ + public static T assertThrows(String message, Class expectedThrowable, + ThrowingSupplier supplier) { + Object result; try { - runnable.run(); + result = supplier.get(); } catch (Throwable actualThrown) { if (expectedThrowable.isInstance(actualThrown)) { @SuppressWarnings("unchecked") T retVal = (T) actualThrown; @@ -1024,13 +1064,27 @@ public static T assertThrows(String message, Class expe throw assertionError; } } - String notThrownMessage = buildPrefix(message) + String - .format("expected %s to be thrown, but nothing was thrown", - formatClass(expectedThrowable)); + String notThrownMessage = buildPrefix(message) + + String.format("expected %s to be thrown, but nothing was thrown", formatClass(expectedThrowable)) + + (result == ThrowingSupplierAdapter.NO_VALUE ? "" : String.format(" (returned %s)", result)); throw new AssertionError(notThrownMessage); } private static String buildPrefix(String message) { return message != null && message.length() != 0 ? message + ": " : ""; } + + private static class ThrowingSupplierAdapter implements ThrowingSupplier { + private static final Object NO_VALUE = new Object(); + private final ThrowingRunnable runnable; + + ThrowingSupplierAdapter(ThrowingRunnable runnable) { + this.runnable = runnable; + } + + public Object get() throws Throwable { + runnable.run(); + return NO_VALUE; + } + } } diff --git a/src/main/java/org/junit/function/ThrowingRunnable.java b/src/main/java/org/junit/function/ThrowingRunnable.java index d0eb782ccd30..20f5dc35d550 100644 --- a/src/main/java/org/junit/function/ThrowingRunnable.java +++ b/src/main/java/org/junit/function/ThrowingRunnable.java @@ -1,13 +1,16 @@ package org.junit.function; /** - * This interface facilitates the use of - * {@link org.junit.Assert#assertThrows(Class, ThrowingRunnable)} from Java 8. It allows method - * references to void methods (that declare checked exceptions) to be passed directly into - * {@code assertThrows} - * without wrapping. It is not meant to be implemented directly. + * Represents an executable operation that may throw a {@code Throwable}. + * + *

This interface facilitates the use of {@link org.junit.Assert#assertThrows(Class, ThrowingRunnable)} + * from Java 8 and above. It allows method references to methods without arguments (that declare checked + * exceptions) to be passed directly into {@code assertThrows} without wrapping. It is not meant to be + * implemented directly. * * @since 4.13 + * @see org.junit.Assert#assertThrows(Class, ThrowingRunnable) + * @see org.junit.Assert#assertThrows(String, Class, ThrowingRunnable) */ public interface ThrowingRunnable { void run() throws Throwable; diff --git a/src/main/java/org/junit/function/ThrowingSupplier.java b/src/main/java/org/junit/function/ThrowingSupplier.java new file mode 100644 index 000000000000..f57ed96071b3 --- /dev/null +++ b/src/main/java/org/junit/function/ThrowingSupplier.java @@ -0,0 +1,17 @@ +package org.junit.function; + +/** + * Represents a supplier of results that may throw a {@code Throwable}. + * + *

This interface facilitates the use of {@link org.junit.Assert#assertThrows(Class, ThrowingSupplier)} + * from Java 8 and above. It allows method references to methods without arguments (that declare checked + * exceptions) to be passed directly into {@code assertThrows} without wrapping. It is not meant to be + * implemented directly. + * + * @since 4.13 + * @see org.junit.Assert#assertThrows(Class, ThrowingSupplier) + * @see org.junit.Assert#assertThrows(String, Class, ThrowingSupplier) + */ +public interface ThrowingSupplier { + T get() throws Throwable; +} diff --git a/src/test/java/org/junit/tests/assertion/AssertionTest.java b/src/test/java/org/junit/tests/assertion/AssertionTest.java index d0c3bdfddc59..f9f162b82a81 100644 --- a/src/test/java/org/junit/tests/assertion/AssertionTest.java +++ b/src/test/java/org/junit/tests/assertion/AssertionTest.java @@ -21,6 +21,7 @@ import org.junit.ComparisonFailure; import org.junit.Test; import org.junit.function.ThrowingRunnable; +import org.junit.function.ThrowingSupplier; import org.junit.internal.ArrayComparisonFailure; /** @@ -853,7 +854,7 @@ public void assertThrowsRequiresAnExceptionToBeThrown() { } @Test - public void assertThrowsIncludesAnInformativeDefaultMessage() { + public void assertThrowsIncludesAnInformativeDefaultMessageForRunnable() { try { assertThrows(Throwable.class, nonThrowingRunnable()); } catch (AssertionError ex) { @@ -864,7 +865,19 @@ public void assertThrowsIncludesAnInformativeDefaultMessage() { } @Test - public void assertThrowsIncludesTheSpecifiedMessage() { + public void assertThrowsIncludesAnInformativeDefaultMessageForSupplier() { + try { + assertThrows(Throwable.class, nonThrowingSupplier("foo")); + } catch (AssertionError ex) { + assertEquals("expected java.lang.Throwable to be thrown, but nothing was thrown (returned foo)", + ex.getMessage()); + return; + } + throw new AssertionError(ASSERTION_ERROR_EXPECTED); + } + + @Test + public void assertThrowsIncludesTheSpecifiedMessageForRunnable() { try { assertThrows("Foobar", Throwable.class, nonThrowingRunnable()); } catch (AssertionError ex) { @@ -877,7 +890,20 @@ public void assertThrowsIncludesTheSpecifiedMessage() { } @Test - public void assertThrowsReturnsTheSameObjectThrown() { + public void assertThrowsIncludesTheSpecifiedMessageForSupplier() { + try { + assertThrows("Foobar", Throwable.class, nonThrowingSupplier("bar")); + } catch (AssertionError ex) { + assertEquals( + "Foobar: expected java.lang.Throwable to be thrown, but nothing was thrown (returned bar)", + ex.getMessage()); + return; + } + throw new AssertionError(ASSERTION_ERROR_EXPECTED); + } + + @Test + public void assertThrowsReturnsTheSameObjectThrownForRunnable() { NullPointerException npe = new NullPointerException(); Throwable throwable = assertThrows(Throwable.class, throwingRunnable(npe)); @@ -885,6 +911,15 @@ public void assertThrowsReturnsTheSameObjectThrown() { assertSame(npe, throwable); } + @Test + public void assertThrowsReturnsTheSameObjectThrownForSupplier() { + NullPointerException npe = new NullPointerException(); + + Throwable throwable = assertThrows(Throwable.class, throwingSupplier(npe)); + + assertSame(npe, throwable); + } + @Test(expected = AssertionError.class) public void assertThrowsDetectsTypeMismatchesViaExplicitTypeHint() { NullPointerException npe = new NullPointerException(); @@ -990,7 +1025,15 @@ private static class NestedException extends RuntimeException { private static ThrowingRunnable nonThrowingRunnable() { return new ThrowingRunnable() { - public void run() throws Throwable { + public void run() { + } + }; + } + + private static ThrowingSupplier nonThrowingSupplier(final T value) { + return new ThrowingSupplier() { + public T get() { + return value; } }; } @@ -1002,4 +1045,12 @@ public void run() throws Throwable { } }; } + + private static ThrowingSupplier throwingSupplier(final Throwable t) { + return new ThrowingSupplier() { + public Void get() throws Throwable { + throw t; + } + }; + } }