Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add assertThrows overloads with ThrowingSupplier #1537

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 58 additions & 4 deletions src/main/java/org/junit/Assert.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -983,6 +984,24 @@ public static <T extends Throwable> T assertThrows(Class<T> 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 extends Throwable> T assertThrows(Class<T> 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
Expand All @@ -999,8 +1018,29 @@ public static <T extends Throwable> T assertThrows(Class<T> expectedThrowable,
*/
public static <T extends Throwable> T assertThrows(String message, Class<T> 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} (<code>null</code>
* 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 extends Throwable> T assertThrows(String message, Class<T> expectedThrowable,
ThrowingSupplier<?> supplier) {
Object result;
try {
runnable.run();
result = supplier.get();
} catch (Throwable actualThrown) {
if (expectedThrowable.isInstance(actualThrown)) {
@SuppressWarnings("unchecked") T retVal = (T) actualThrown;
Expand All @@ -1024,13 +1064,27 @@ public static <T extends Throwable> T assertThrows(String message, Class<T> 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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the object returned by the lambda throws an exception when you call toString() on it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't handle that case anywhere, do we? But we should probably call formatClassAndValue(). I can make this change but I think we should agree whether to proceed with this PR first.

@kcooney WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that toString() for arrays ends up being ugly, in case the return value is an array.

I addressed that in JUnit Jupiter like this: junit-team/junit5@c5c4741

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't handle that case anywhere, do we?

No, I don't think there is any exception handling for invocations of toString() on objects supplied to assertion methods in either JUnit 4 or JUnit Jupiter.

throw new AssertionError(notThrownMessage);
}

private static String buildPrefix(String message) {
return message != null && message.length() != 0 ? message + ": " : "";
}

private static class ThrowingSupplierAdapter implements ThrowingSupplier<Object> {
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;
}
}
}
13 changes: 8 additions & 5 deletions src/main/java/org/junit/function/ThrowingRunnable.java
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>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;
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/org/junit/function/ThrowingSupplier.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.junit.function;

/**
* Represents a supplier of results that may throw a {@code Throwable}.
*
* <p>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> {
T get() throws Throwable;
}
59 changes: 55 additions & 4 deletions src/test/java/org/junit/tests/assertion/AssertionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -853,7 +854,7 @@ public void assertThrowsRequiresAnExceptionToBeThrown() {
}

@Test
public void assertThrowsIncludesAnInformativeDefaultMessage() {
public void assertThrowsIncludesAnInformativeDefaultMessageForRunnable() {
try {
assertThrows(Throwable.class, nonThrowingRunnable());
} catch (AssertionError ex) {
Expand All @@ -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) {
Expand All @@ -877,14 +890,36 @@ 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));

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();
Expand Down Expand Up @@ -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 <T> ThrowingSupplier<T> nonThrowingSupplier(final T value) {
return new ThrowingSupplier<T>() {
public T get() {
return value;
}
};
}
Expand All @@ -1002,4 +1045,12 @@ public void run() throws Throwable {
}
};
}

private static ThrowingSupplier<?> throwingSupplier(final Throwable t) {
return new ThrowingSupplier<Void>() {
public Void get() throws Throwable {
throw t;
}
};
}
}