Skip to content

Commit

Permalink
hamcrest#329 Added declarative equivalent of JUnit's assertThrows
Browse files Browse the repository at this point in the history
  • Loading branch information
peterdemaeyer committed Feb 21, 2021
1 parent 8522353 commit 7844e8d
Show file tree
Hide file tree
Showing 5 changed files with 316 additions and 2 deletions.
7 changes: 7 additions & 0 deletions hamcrest/src/main/java/org/hamcrest/Executable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.hamcrest;

@FunctionalInterface
public interface Executable {

void execute() throws Throwable;
}
29 changes: 28 additions & 1 deletion hamcrest/src/main/java/org/hamcrest/MatcherAssert.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,37 @@ public static <T> void assertThat(String reason, T actual, Matcher<? super T> ma
throw new AssertionError(description.toString());
}
}

public static void assertThat(String reason, boolean assertion) {
if (!assertion) {
throw new AssertionError(reason);
}
}

public static <T extends Throwable> void assertThat(Executable executable, Throws<T> doesThrow) {
assertThat("", executable, doesThrow);
}

public static <T extends Throwable> void assertThat(String reason, Executable executable, Throws<T> 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());
}
}
}
}
66 changes: 66 additions & 0 deletions hamcrest/src/main/java/org/hamcrest/Matchers.java
Original file line number Diff line number Diff line change
Expand Up @@ -1712,5 +1712,71 @@ public static org.hamcrest.Matcher<org.w3c.dom.Node> 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:
* <pre>assertThat(() -> methodCallThatThrowsException(), doesThrow(withMessage("file not found")))</pre>
*
* @param throwableMatcher
* the matcher for the throwable to match, which must not be {@code null}
*/
public static <T extends Throwable> Throws<T> doesThrow(Matcher<? super Throwable> 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:
* <pre>assertThat(() -> methodCallThatThrowsIOException(), throwsInstanceOf(IOException.class))</pre>
* 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 <T extends Throwable> Throws<T> throwsInstanceOf(Class<T> 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:
* <pre>assertThat(() -> methodCallThatThrowsException(), doesThrow(withMessage(startsWith("file not found"))))</pre>
*
* @param messageMatcher
* the matcher to match the throwable's message with, which must not be {@code null}
*/
public static <T extends Throwable> Matcher<T> withMessage(Matcher<String> 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:
* <pre>assertThat(() -> methodCallThatThrowsException(), doesThrow(withMessage("message")))</pre>
* 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 <T extends Throwable> Matcher<T> 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:
* <pre>assertThat(() -> methodCallThatThrowsInvocationTargetException(), doesThrow(becauseOf(instanceOf(IOException.class))))</pre>
*
* @param causeMatcher
* the matcher to matcher the outer throwable's inner cause with, which must not be {@code null}
*/
public static <T extends Throwable> Matcher<T> becauseOf(Matcher<? extends Throwable> causeMatcher) {
return Throws.becauseOf(causeMatcher);
}
}
100 changes: 100 additions & 0 deletions hamcrest/src/main/java/org/hamcrest/Throws.java
Original file line number Diff line number Diff line change
@@ -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<T extends Throwable> implements SelfDescribing {

private final Matcher<? super T> matcher;

public Throws(Matcher<? super T> throwableMatcher) {
requireNonNull(throwableMatcher);
this.matcher = new BaseMatcher<T>() {

@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<? super T> asMatcher() {
return matcher;
}

@Override
public void describeTo(Description description) {
matcher.describeTo(description);
}

public void describeMismatch(Description description) {
description.appendText("did not throw");
}

public static <T extends Throwable> Matcher<T> withMessage(Matcher<String> messageMatcher) {
return new TypeSafeMatcher<T>() {

@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 <T extends Throwable> Matcher<T> becauseOf(Matcher<? extends Throwable> causeMatcher) {
return new TypeSafeMatcher<T>() {

@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 <T extends Throwable> Throws<T> doesThrow(Matcher<? super T> throwableMatcher) {
return new Throws<T>(throwableMatcher);
}

public static <T extends Throwable> Throws<T> throwsInstanceOf(Class<T> throwableType) {
return doesThrow(instanceOf(throwableType));
}
}
116 changes: 115 additions & 1 deletion hamcrest/src/test/java/org/hamcrest/MatcherAssertTest.java
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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 <java.lang.IllegalStateException> 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 <java.lang.IllegalArgumentException> 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 <java.lang.IllegalArgumentException> 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());
}
}
}

0 comments on commit 7844e8d

Please sign in to comment.