diff --git a/hamcrest/src/main/java/org/hamcrest/Executable.java b/hamcrest/src/main/java/org/hamcrest/Executable.java new file mode 100644 index 00000000..fb964219 --- /dev/null +++ b/hamcrest/src/main/java/org/hamcrest/Executable.java @@ -0,0 +1,7 @@ +package org.hamcrest; + +@FunctionalInterface +public interface Executable { + + void execute() throws Throwable; +} diff --git a/hamcrest/src/main/java/org/hamcrest/MatcherAssert.java b/hamcrest/src/main/java/org/hamcrest/MatcherAssert.java index bc001ebb..e52b9858 100644 --- a/hamcrest/src/main/java/org/hamcrest/MatcherAssert.java +++ b/hamcrest/src/main/java/org/hamcrest/MatcherAssert.java @@ -20,10 +20,37 @@ public static void assertThat(String reason, T actual, Matcher ma throw new AssertionError(description.toString()); } } - + public static void assertThat(String reason, boolean assertion) { if (!assertion) { throw new AssertionError(reason); } } + + public static void assertThat(Executable executable, Throws doesThrow) { + assertThat("", executable, doesThrow); + } + + public static void assertThat(String reason, Executable executable, Throws doesThrow) { + boolean executionDidNotThrow = false; + try { + executable.execute(); + executionDidNotThrow = true; + } catch (Throwable actual) { + assertThat(reason, (T) actual, doesThrow.asMatcher()); + } finally { + if (executionDidNotThrow) { + Description description = new StringDescription(); + description.appendText(reason) + .appendText(System.lineSeparator()) + .appendText("Expected: ") + .appendDescriptionOf(doesThrow) + .appendText(System.lineSeparator()) + .appendText(" but: "); + doesThrow.describeMismatch(description); + + throw new AssertionError(description.toString()); + } + } + } } diff --git a/hamcrest/src/main/java/org/hamcrest/Matchers.java b/hamcrest/src/main/java/org/hamcrest/Matchers.java index d4f543ea..32125b9b 100644 --- a/hamcrest/src/main/java/org/hamcrest/Matchers.java +++ b/hamcrest/src/main/java/org/hamcrest/Matchers.java @@ -1712,5 +1712,71 @@ public static org.hamcrest.Matcher hasXPath(java.lang.String x return org.hamcrest.xml.HasXPath.hasXPath(xPath, namespaceContext); } + /** + * Creates a {@link Throws} object that matches a throwable according to the given throwable matcher. + * This can be used with the {@link MatcherAssert#assertThat(String, Executable, Throws)} family of methods. + * For example: + *
assertThat(() -> methodCallThatThrowsException(), doesThrow(withMessage("file not found")))
+ * + * @param throwableMatcher + * the matcher for the throwable to match, which must not be {@code null} + */ + public static Throws doesThrow(Matcher throwableMatcher) { + return Throws.doesThrow(throwableMatcher); + } + + /** + * Creates a {@link Throws} object that matches a throwable of the given type. + * This can be used with the {@link MatcherAssert#assertThat(String, Executable, Throws)} family of methods. + * For example: + *
assertThat(() -> methodCallThatThrowsIOException(), throwsInstanceOf(IOException.class))
+ * This is shorthand for {@code doesThrow(instanceOf(MyThrowable.class))}, to be used as equivalent for JUnit 5's + * {@code assertThrows(MyThrowable.class, () -> {})}. + * + * @param throwableType + * the type of the throwable to match, which must not be {@code null} + */ + public static Throws throwsInstanceOf(Class throwableType) { + return Throws.throwsInstanceOf(throwableType); + } + + /** + * Creates a matcher that matches a throwable by matching its message. + * This can be used with the {@link MatcherAssert#assertThat(String, Executable, Throws)} family of methods. + * For example: + *
assertThat(() -> methodCallThatThrowsException(), doesThrow(withMessage(startsWith("file not found"))))
+ * + * @param messageMatcher + * the matcher to match the throwable's message with, which must not be {@code null} + */ + public static Matcher withMessage(Matcher messageMatcher) { + return Throws.withMessage(messageMatcher); + } + + /** + * Creates a matcher that matches a throwable by its message. + * This can be used with the {@link MatcherAssert#assertThat(String, Executable, Throws)} family of methods. + * For example: + *
assertThat(() -> methodCallThatThrowsException(), doesThrow(withMessage("message")))
+ * This is shorthand for {@code doesThrow(withMessage(equalTo("message")))}. + * + * @param messageToMatch + * the message to match the throwable's message with, which must not be {@code null} + */ + public static Matcher withMessage(String messageToMatch) { + return withMessage(equalTo(messageToMatch)); + } + /** + * Creates a matcher that matches an outer throwable by matching its inner cause. + * This can be used with the {@link MatcherAssert#assertThat(String, Executable, Throws)} family of methods. + * For example: + *
assertThat(() -> methodCallThatThrowsInvocationTargetException(), doesThrow(becauseOf(instanceOf(IOException.class))))
+ * + * @param causeMatcher + * the matcher to matcher the outer throwable's inner cause with, which must not be {@code null} + */ + public static Matcher becauseOf(Matcher causeMatcher) { + return Throws.becauseOf(causeMatcher); + } } diff --git a/hamcrest/src/main/java/org/hamcrest/Throws.java b/hamcrest/src/main/java/org/hamcrest/Throws.java new file mode 100644 index 00000000..fa64b9eb --- /dev/null +++ b/hamcrest/src/main/java/org/hamcrest/Throws.java @@ -0,0 +1,100 @@ +package org.hamcrest; + +import static java.util.Objects.requireNonNull; +import static org.hamcrest.core.IsInstanceOf.instanceOf; + +/** + * @author Peter De Maeyer + */ +public final class Throws implements SelfDescribing { + + private final Matcher matcher; + + public Throws(Matcher throwableMatcher) { + requireNonNull(throwableMatcher); + this.matcher = new BaseMatcher() { + + @Override + public boolean matches(Object actual) { + return throwableMatcher.matches(actual); + } + + @Override + public void describeTo(Description description) { + description.appendText("throws "); + throwableMatcher.describeTo(description); + } + + @Override + public void describeMismatch(Object item, Description mismatchDescription) { + mismatchDescription.appendText("threw but "); + throwableMatcher.describeMismatch(item, mismatchDescription); + } + }; + } + + Matcher asMatcher() { + return matcher; + } + + @Override + public void describeTo(Description description) { + matcher.describeTo(description); + } + + public void describeMismatch(Description description) { + description.appendText("did not throw"); + } + + public static Matcher withMessage(Matcher messageMatcher) { + return new TypeSafeMatcher() { + + @Override + protected boolean matchesSafely(T throwable) { + return messageMatcher.matches(throwable.getMessage()); + } + + @Override + public void describeTo(Description description) { + description.appendText("with message "); + messageMatcher.describeTo(description); + } + + @Override + protected void describeMismatchSafely(T item, Description mismatchDescription) { + mismatchDescription.appendText("message "); + messageMatcher.describeMismatch(item.getMessage(), mismatchDescription); + } + }; + } + + public static Matcher becauseOf(Matcher causeMatcher) { + return new TypeSafeMatcher() { + + @Override + protected boolean matchesSafely(T throwable) { + return causeMatcher.matches(throwable.getCause()); + } + + @Override + public void describeTo(Description description) { + description.appendText("because of "); + causeMatcher.describeTo(description); + } + + @Override + protected void describeMismatchSafely(T item, Description mismatchDescription) { + mismatchDescription.appendText("cause "); + causeMatcher.describeMismatch(item.getCause(), mismatchDescription); + } + }; + } + + public static Throws doesThrow(Matcher throwableMatcher) { + return new Throws(throwableMatcher); + } + + public static Throws throwsInstanceOf(Class throwableType) { + return doesThrow(instanceOf(throwableType)); + } +} diff --git a/hamcrest/src/test/java/org/hamcrest/MatcherAssertTest.java b/hamcrest/src/test/java/org/hamcrest/MatcherAssertTest.java index 8d88daa8..3110d3fb 100644 --- a/hamcrest/src/test/java/org/hamcrest/MatcherAssertTest.java +++ b/hamcrest/src/test/java/org/hamcrest/MatcherAssertTest.java @@ -1,9 +1,12 @@ package org.hamcrest; +import java.io.IOException; +import javax.xml.stream.XMLStreamException; + import org.junit.Test; +import static org.hamcrest.Matchers.*; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.Assert.*; public final class MatcherAssertTest { @@ -96,4 +99,115 @@ public void describeMismatch(Object item, Description mismatchDescription) { canAssertSubtypes() { assertThat(1, equalTo((Number) 1)); } + + @Test public void + throwableIsOfMatchingInstance() { + assertThat( + () -> { throw new IllegalStateException(); }, + throwsInstanceOf(IllegalStateException.class) + ); + } + + @Test public void + throwableIsNotOfMatchingInstance() { + String endLine = System.lineSeparator(); + String expectedMessage = endLine + "Expected: throws an instance of java.io.IOException" + endLine + + " but: threw but is a java.lang.IllegalStateException"; + try { + assertThat( + () -> { throw new IllegalStateException(); }, + throwsInstanceOf(IOException.class) + ); + fail("should have failed"); + } + catch (AssertionError e) { + assertEquals(expectedMessage, e.getMessage()); + } + } + + @Test public void + throwableHasMatchingMessage() { + assertThat( + () -> { throw new Exception("message"); }, + doesThrow(withMessage(equalTo("message"))) + ); + } + + @Test public void + throwableDoesNotHaveMatchingMessage() { + String endLine = System.lineSeparator(); + String expectedMessage = endLine + "Expected: throws with message \"expected message\"" + endLine + + " but: threw but message was \"actual message\""; + try { + assertThat( + () -> { throw new Exception("actual message"); }, + doesThrow(withMessage("expected message")) + ); + fail("should have failed"); + } + catch (AssertionError e) { + assertEquals(expectedMessage, e.getMessage()); + } + } + + @Test public void + throwableExecutionDoesNotThrow() { + String endLine = System.lineSeparator(); + String expectedMessage = endLine + "Expected: throws an instance of java.lang.NoSuchMethodError" + + endLine + " but: did not throw"; + try { + assertThat( + () -> {}, // Do nothing + throwsInstanceOf(NoSuchMethodError.class) + ); + fail("should have failed"); + } + catch (AssertionError e) { + assertEquals(expectedMessage, e.getMessage()); + } + } + + @Test public void + throwableCauseMatches() { + assertThat( + () -> { throw new RuntimeException(new XMLStreamException()); }, + doesThrow(becauseOf(instanceOf(XMLStreamException.class))) + ); + } + + @Test public void + throwableCauseDoesNotMatch() { + String endLine = System.lineSeparator(); + String expectedMessage = endLine + "Expected: throws because of an instance of java.lang.NullPointerException" + + endLine + " but: threw but cause is a java.lang.IllegalArgumentException"; + try { + assertThat( + () -> { throw new RuntimeException(new IllegalArgumentException()); }, + doesThrow(becauseOf(instanceOf(NullPointerException.class))) + ); + fail("should have failed"); + } + catch (AssertionError e) { + assertEquals(expectedMessage, e.getMessage()); + } + } + + @Test public void + throwableExecutionDoesNotMatchWithCustomMessage() { + String endLine = System.lineSeparator(); + String expectedMessage = "Custom message" + + endLine + "Expected: throws an instance of java.lang.NullPointerException" + + endLine + " but: threw but is a java.lang.IllegalArgumentException"; + try { + assertThat( + "Custom message", + () -> { throw new IllegalArgumentException(); }, + throwsInstanceOf(NullPointerException.class) + ); + fail("should have failed"); + } + catch (AssertionError e) { + assertEquals(expectedMessage, e.getMessage()); + } + } }