Skip to content

Commit 7844e8d

Browse files
committed
#329 Added declarative equivalent of JUnit's assertThrows
1 parent 8522353 commit 7844e8d

File tree

5 files changed

+316
-2
lines changed

5 files changed

+316
-2
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.hamcrest;
2+
3+
@FunctionalInterface
4+
public interface Executable {
5+
6+
void execute() throws Throwable;
7+
}

hamcrest/src/main/java/org/hamcrest/MatcherAssert.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,37 @@ public static <T> void assertThat(String reason, T actual, Matcher<? super T> ma
2020
throw new AssertionError(description.toString());
2121
}
2222
}
23-
23+
2424
public static void assertThat(String reason, boolean assertion) {
2525
if (!assertion) {
2626
throw new AssertionError(reason);
2727
}
2828
}
29+
30+
public static <T extends Throwable> void assertThat(Executable executable, Throws<T> doesThrow) {
31+
assertThat("", executable, doesThrow);
32+
}
33+
34+
public static <T extends Throwable> void assertThat(String reason, Executable executable, Throws<T> doesThrow) {
35+
boolean executionDidNotThrow = false;
36+
try {
37+
executable.execute();
38+
executionDidNotThrow = true;
39+
} catch (Throwable actual) {
40+
assertThat(reason, (T) actual, doesThrow.asMatcher());
41+
} finally {
42+
if (executionDidNotThrow) {
43+
Description description = new StringDescription();
44+
description.appendText(reason)
45+
.appendText(System.lineSeparator())
46+
.appendText("Expected: ")
47+
.appendDescriptionOf(doesThrow)
48+
.appendText(System.lineSeparator())
49+
.appendText(" but: ");
50+
doesThrow.describeMismatch(description);
51+
52+
throw new AssertionError(description.toString());
53+
}
54+
}
55+
}
2956
}

hamcrest/src/main/java/org/hamcrest/Matchers.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1712,5 +1712,71 @@ public static org.hamcrest.Matcher<org.w3c.dom.Node> hasXPath(java.lang.String x
17121712
return org.hamcrest.xml.HasXPath.hasXPath(xPath, namespaceContext);
17131713
}
17141714

1715+
/**
1716+
* Creates a {@link Throws} object that matches a throwable according to the given throwable matcher.
1717+
* This can be used with the {@link MatcherAssert#assertThat(String, Executable, Throws)} family of methods.
1718+
* For example:
1719+
* <pre>assertThat(() -> methodCallThatThrowsException(), doesThrow(withMessage("file not found")))</pre>
1720+
*
1721+
* @param throwableMatcher
1722+
* the matcher for the throwable to match, which must not be {@code null}
1723+
*/
1724+
public static <T extends Throwable> Throws<T> doesThrow(Matcher<? super Throwable> throwableMatcher) {
1725+
return Throws.doesThrow(throwableMatcher);
1726+
}
1727+
1728+
/**
1729+
* Creates a {@link Throws} object that matches a throwable of the given type.
1730+
* This can be used with the {@link MatcherAssert#assertThat(String, Executable, Throws)} family of methods.
1731+
* For example:
1732+
* <pre>assertThat(() -> methodCallThatThrowsIOException(), throwsInstanceOf(IOException.class))</pre>
1733+
* This is shorthand for {@code doesThrow(instanceOf(MyThrowable.class))}, to be used as equivalent for JUnit 5's
1734+
* {@code assertThrows(MyThrowable.class, () -> {})}.
1735+
*
1736+
* @param throwableType
1737+
* the type of the throwable to match, which must not be {@code null}
1738+
*/
1739+
public static <T extends Throwable> Throws<T> throwsInstanceOf(Class<T> throwableType) {
1740+
return Throws.throwsInstanceOf(throwableType);
1741+
}
1742+
1743+
/**
1744+
* Creates a matcher that matches a throwable by matching its message.
1745+
* This can be used with the {@link MatcherAssert#assertThat(String, Executable, Throws)} family of methods.
1746+
* For example:
1747+
* <pre>assertThat(() -> methodCallThatThrowsException(), doesThrow(withMessage(startsWith("file not found"))))</pre>
1748+
*
1749+
* @param messageMatcher
1750+
* the matcher to match the throwable's message with, which must not be {@code null}
1751+
*/
1752+
public static <T extends Throwable> Matcher<T> withMessage(Matcher<String> messageMatcher) {
1753+
return Throws.withMessage(messageMatcher);
1754+
}
1755+
1756+
/**
1757+
* Creates a matcher that matches a throwable by its message.
1758+
* This can be used with the {@link MatcherAssert#assertThat(String, Executable, Throws)} family of methods.
1759+
* For example:
1760+
* <pre>assertThat(() -> methodCallThatThrowsException(), doesThrow(withMessage("message")))</pre>
1761+
* This is shorthand for {@code doesThrow(withMessage(equalTo("message")))}.
1762+
*
1763+
* @param messageToMatch
1764+
* the message to match the throwable's message with, which must not be {@code null}
1765+
*/
1766+
public static <T extends Throwable> Matcher<T> withMessage(String messageToMatch) {
1767+
return withMessage(equalTo(messageToMatch));
1768+
}
17151769

1770+
/**
1771+
* Creates a matcher that matches an outer throwable by matching its inner cause.
1772+
* This can be used with the {@link MatcherAssert#assertThat(String, Executable, Throws)} family of methods.
1773+
* For example:
1774+
* <pre>assertThat(() -> methodCallThatThrowsInvocationTargetException(), doesThrow(becauseOf(instanceOf(IOException.class))))</pre>
1775+
*
1776+
* @param causeMatcher
1777+
* the matcher to matcher the outer throwable's inner cause with, which must not be {@code null}
1778+
*/
1779+
public static <T extends Throwable> Matcher<T> becauseOf(Matcher<? extends Throwable> causeMatcher) {
1780+
return Throws.becauseOf(causeMatcher);
1781+
}
17161782
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package org.hamcrest;
2+
3+
import static java.util.Objects.requireNonNull;
4+
import static org.hamcrest.core.IsInstanceOf.instanceOf;
5+
6+
/**
7+
* @author Peter De Maeyer
8+
*/
9+
public final class Throws<T extends Throwable> implements SelfDescribing {
10+
11+
private final Matcher<? super T> matcher;
12+
13+
public Throws(Matcher<? super T> throwableMatcher) {
14+
requireNonNull(throwableMatcher);
15+
this.matcher = new BaseMatcher<T>() {
16+
17+
@Override
18+
public boolean matches(Object actual) {
19+
return throwableMatcher.matches(actual);
20+
}
21+
22+
@Override
23+
public void describeTo(Description description) {
24+
description.appendText("throws ");
25+
throwableMatcher.describeTo(description);
26+
}
27+
28+
@Override
29+
public void describeMismatch(Object item, Description mismatchDescription) {
30+
mismatchDescription.appendText("threw but ");
31+
throwableMatcher.describeMismatch(item, mismatchDescription);
32+
}
33+
};
34+
}
35+
36+
Matcher<? super T> asMatcher() {
37+
return matcher;
38+
}
39+
40+
@Override
41+
public void describeTo(Description description) {
42+
matcher.describeTo(description);
43+
}
44+
45+
public void describeMismatch(Description description) {
46+
description.appendText("did not throw");
47+
}
48+
49+
public static <T extends Throwable> Matcher<T> withMessage(Matcher<String> messageMatcher) {
50+
return new TypeSafeMatcher<T>() {
51+
52+
@Override
53+
protected boolean matchesSafely(T throwable) {
54+
return messageMatcher.matches(throwable.getMessage());
55+
}
56+
57+
@Override
58+
public void describeTo(Description description) {
59+
description.appendText("with message ");
60+
messageMatcher.describeTo(description);
61+
}
62+
63+
@Override
64+
protected void describeMismatchSafely(T item, Description mismatchDescription) {
65+
mismatchDescription.appendText("message ");
66+
messageMatcher.describeMismatch(item.getMessage(), mismatchDescription);
67+
}
68+
};
69+
}
70+
71+
public static <T extends Throwable> Matcher<T> becauseOf(Matcher<? extends Throwable> causeMatcher) {
72+
return new TypeSafeMatcher<T>() {
73+
74+
@Override
75+
protected boolean matchesSafely(T throwable) {
76+
return causeMatcher.matches(throwable.getCause());
77+
}
78+
79+
@Override
80+
public void describeTo(Description description) {
81+
description.appendText("because of ");
82+
causeMatcher.describeTo(description);
83+
}
84+
85+
@Override
86+
protected void describeMismatchSafely(T item, Description mismatchDescription) {
87+
mismatchDescription.appendText("cause ");
88+
causeMatcher.describeMismatch(item.getCause(), mismatchDescription);
89+
}
90+
};
91+
}
92+
93+
public static <T extends Throwable> Throws<T> doesThrow(Matcher<? super T> throwableMatcher) {
94+
return new Throws<T>(throwableMatcher);
95+
}
96+
97+
public static <T extends Throwable> Throws<T> throwsInstanceOf(Class<T> throwableType) {
98+
return doesThrow(instanceOf(throwableType));
99+
}
100+
}

hamcrest/src/test/java/org/hamcrest/MatcherAssertTest.java

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package org.hamcrest;
22

3+
import java.io.IOException;
4+
import javax.xml.stream.XMLStreamException;
5+
36
import org.junit.Test;
47

8+
import static org.hamcrest.Matchers.*;
59
import static org.hamcrest.MatcherAssert.assertThat;
6-
import static org.hamcrest.core.IsEqual.equalTo;
710
import static org.junit.Assert.*;
811

912
public final class MatcherAssertTest {
@@ -96,4 +99,115 @@ public void describeMismatch(Object item, Description mismatchDescription) {
9699
canAssertSubtypes() {
97100
assertThat(1, equalTo((Number) 1));
98101
}
102+
103+
@Test public void
104+
throwableIsOfMatchingInstance() {
105+
assertThat(
106+
() -> { throw new IllegalStateException(); },
107+
throwsInstanceOf(IllegalStateException.class)
108+
);
109+
}
110+
111+
@Test public void
112+
throwableIsNotOfMatchingInstance() {
113+
String endLine = System.lineSeparator();
114+
String expectedMessage = endLine + "Expected: throws an instance of java.io.IOException" + endLine
115+
+ " but: threw but <java.lang.IllegalStateException> is a java.lang.IllegalStateException";
116+
try {
117+
assertThat(
118+
() -> { throw new IllegalStateException(); },
119+
throwsInstanceOf(IOException.class)
120+
);
121+
fail("should have failed");
122+
}
123+
catch (AssertionError e) {
124+
assertEquals(expectedMessage, e.getMessage());
125+
}
126+
}
127+
128+
@Test public void
129+
throwableHasMatchingMessage() {
130+
assertThat(
131+
() -> { throw new Exception("message"); },
132+
doesThrow(withMessage(equalTo("message")))
133+
);
134+
}
135+
136+
@Test public void
137+
throwableDoesNotHaveMatchingMessage() {
138+
String endLine = System.lineSeparator();
139+
String expectedMessage = endLine + "Expected: throws with message \"expected message\"" + endLine
140+
+ " but: threw but message was \"actual message\"";
141+
try {
142+
assertThat(
143+
() -> { throw new Exception("actual message"); },
144+
doesThrow(withMessage("expected message"))
145+
);
146+
fail("should have failed");
147+
}
148+
catch (AssertionError e) {
149+
assertEquals(expectedMessage, e.getMessage());
150+
}
151+
}
152+
153+
@Test public void
154+
throwableExecutionDoesNotThrow() {
155+
String endLine = System.lineSeparator();
156+
String expectedMessage = endLine + "Expected: throws an instance of java.lang.NoSuchMethodError"
157+
+ endLine + " but: did not throw";
158+
try {
159+
assertThat(
160+
() -> {}, // Do nothing
161+
throwsInstanceOf(NoSuchMethodError.class)
162+
);
163+
fail("should have failed");
164+
}
165+
catch (AssertionError e) {
166+
assertEquals(expectedMessage, e.getMessage());
167+
}
168+
}
169+
170+
@Test public void
171+
throwableCauseMatches() {
172+
assertThat(
173+
() -> { throw new RuntimeException(new XMLStreamException()); },
174+
doesThrow(becauseOf(instanceOf(XMLStreamException.class)))
175+
);
176+
}
177+
178+
@Test public void
179+
throwableCauseDoesNotMatch() {
180+
String endLine = System.lineSeparator();
181+
String expectedMessage = endLine + "Expected: throws because of an instance of java.lang.NullPointerException"
182+
+ endLine + " but: threw but cause <java.lang.IllegalArgumentException> is a java.lang.IllegalArgumentException";
183+
try {
184+
assertThat(
185+
() -> { throw new RuntimeException(new IllegalArgumentException()); },
186+
doesThrow(becauseOf(instanceOf(NullPointerException.class)))
187+
);
188+
fail("should have failed");
189+
}
190+
catch (AssertionError e) {
191+
assertEquals(expectedMessage, e.getMessage());
192+
}
193+
}
194+
195+
@Test public void
196+
throwableExecutionDoesNotMatchWithCustomMessage() {
197+
String endLine = System.lineSeparator();
198+
String expectedMessage = "Custom message"
199+
+ endLine + "Expected: throws an instance of java.lang.NullPointerException"
200+
+ endLine + " but: threw but <java.lang.IllegalArgumentException> is a java.lang.IllegalArgumentException";
201+
try {
202+
assertThat(
203+
"Custom message",
204+
() -> { throw new IllegalArgumentException(); },
205+
throwsInstanceOf(NullPointerException.class)
206+
);
207+
fail("should have failed");
208+
}
209+
catch (AssertionError e) {
210+
assertEquals(expectedMessage, e.getMessage());
211+
}
212+
}
99213
}

0 commit comments

Comments
 (0)