I couldn't implement it in a nice way if counting assertion error descriptions (probably this is why Hamcrest does not provide such a feature), but if you're playing well with Java 8 then you might want something like this (however I don't think it would be ever used because of the issues described below):
IThrowingRunnable
This interface is used to wrap code that could potentially throw exceptions. Callable<E>
might be used as well, but the latter requires a value to be returned, so I think that a runnable ("void-callable") is more convenient.
@FunctionalInterface
public interface IThrowingRunnable<E extends Throwable> {
void run()
throws E;
}
FailsWithMatcher
This class implements a matcher that requires the given callback to throw an exception. A disadvantage of this implementation is that having a callback throwing an unexpected exception (or even not throwing a single) does not describe what's wrong and you'd see totally obscure error messages.
public final class FailsWithMatcher<EX extends Throwable>
extends TypeSafeMatcher<IThrowingRunnable<EX>> {
private final Matcher<? super EX> matcher;
private FailsWithMatcher(final Matcher<? super EX> matcher) {
this.matcher = matcher;
}
public static <EX extends Throwable> Matcher<IThrowingRunnable<EX>> failsWith(final Class<EX> throwableType) {
return new FailsWithMatcher<>(instanceOf(throwableType));
}
public static <EX extends Throwable> Matcher<IThrowingRunnable<EX>> failsWith(final Class<EX> throwableType, final Matcher<? super EX> throwableMatcher) {
return new FailsWithMatcher<>(allOf(instanceOf(throwableType), throwableMatcher));
}
@Override
protected boolean matchesSafely(final IThrowingRunnable<EX> runnable) {
try {
runnable.run();
return false;
} catch ( final Throwable ex ) {
return matcher.matches(ex);
}
}
@Override
public void describeTo(final Description description) {
description.appendText("fails with ").appendDescriptionOf(matcher);
}
}
ExceptionMessageMatcher
This is a sample matcher to make a simple check for the thrown exception message.
public final class ExceptionMessageMatcher<EX extends Throwable>
extends TypeSafeMatcher<EX> {
private final Matcher<? super String> matcher;
private ExceptionMessageMatcher(final Matcher<String> matcher) {
this.matcher = matcher;
}
public static <EX extends Throwable> Matcher<EX> exceptionMessage(final String message) {
return new ExceptionMessageMatcher<>(is(message));
}
@Override
protected boolean matchesSafely(final EX ex) {
return matcher.matches(ex.getMessage());
}
@Override
public void describeTo(final Description description) {
description.appendDescriptionOf(matcher);
}
}
And the test sample itself
@Test
public void test() {
assertThat(() -> emptyList().get(0), failsWith(IndexOutOfBoundsException.class, exceptionMessage("Index: 0")));
assertThat(() -> emptyList().set(0, null), failsWith(UnsupportedOperationException.class));
}
Note that this approach:
- ... is test-runner-independent
- ... allows to specify multiple assertions in a single test
And the worst thing, a typical fail will look like
java.lang.AssertionError:
Expected: fails with (an instance of java.lang.IndexOutOfBoundsException and is "Index: 0001")
but: was <foo.bar.baz.FailsWithMatcherTest$$Lambda$1/127618319@6b143ee9>
Maybe using a custom implementation of the assertThat()
method could fix it.