Skip to content

Commit

Permalink
Add matcher for thrown exceptions in Runnable (#423)
Browse files Browse the repository at this point in the history
  • Loading branch information
ggalmazor authored Nov 3, 2024
1 parent 1bed2f5 commit 1e4230f
Show file tree
Hide file tree
Showing 3 changed files with 254 additions and 15 deletions.
103 changes: 88 additions & 15 deletions hamcrest/src/main/java/org/hamcrest/Matchers.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.hamcrest.collection.ArrayMatching;
import org.hamcrest.core.IsIterableContaining;
import org.hamcrest.core.StringRegularExpression;
import org.hamcrest.exception.ThrowsException;
import org.hamcrest.optional.OptionalEmpty;
import org.hamcrest.optional.OptionalWithValue;
import org.hamcrest.text.IsEqualCompressingWhiteSpace;
Expand Down Expand Up @@ -1972,21 +1973,21 @@ public static Matcher<CharSequence> hasLength(org.hamcrest.Matcher<? super java.
return org.hamcrest.text.CharSequenceLength.hasLength(lengthMatcher);
}

/**
* Creates a matcher of {@link CharSequence} that matches when a char sequence has the length
* of the specified <code>argument</code>.
* For example:
*
* <pre>
* assertThat("text", length(4))
* </pre>
*
* @param length the expected length of the string
* @return The matcher.
*/
public static Matcher<CharSequence> hasLength(int length) {
return org.hamcrest.text.CharSequenceLength.hasLength(length);
}
/**
* Creates a matcher of {@link CharSequence} that matches when a char sequence has the length
* of the specified <code>argument</code>.
* For example:
*
* <pre>
* assertThat("text", length(4))
* </pre>
*
* @param length the expected length of the string
* @return The matcher.
*/
public static Matcher<CharSequence> hasLength(int length) {
return org.hamcrest.text.CharSequenceLength.hasLength(length);
}

/**
* Creates a matcher that matches any examined object whose <code>toString</code> method
Expand Down Expand Up @@ -2228,4 +2229,76 @@ public static <T> Matcher<Optional<T>> optionalWithValue(T value) {
public static <T> Matcher<Optional<T>> optionalWithValue(Matcher<? super T> matcher) {
return OptionalWithValue.optionalWithValue(matcher);
}

/**
* Matcher for {@link Throwable} that expects that the Runnable throws an exception
*
* @param <T> type of the Runnable
* @return The matcher.
*/
public static <T extends Runnable> ThrowsException<T> throwsException() {
return ThrowsException.throwsException();
}

/**
* Matcher for {@link Throwable} that expects that the Runnable throws an exception of the provided <code>throwableClass</code> class
*
* @param <U> type of the Runnable
* @param <T> type of the Throwable
* @param throwableClass the Throwable class against which examined exceptions are compared
* @return The matcher.
*/
public static <T extends Runnable, U extends Throwable> ThrowsException<T> throwsException(Class<U> throwableClass) {
return ThrowsException.throwsException(throwableClass);
}

/**
* Matcher for {@link Throwable} that expects that the Runnable throws an exception of the provided <code>throwableClass</code> class and has a message equal to the provided <code>message</code>
*
* @param <T> type of the Runnable
* @param <U> type of the Throwable
* @param throwableClass the Throwable class against which examined exceptions are compared
* @param message the String against which examined exception messages are compared
* @return The matcher.
*/
public static <T extends Runnable, U extends Throwable> ThrowsException<T> throwsException(Class<U> throwableClass, String message) {
return ThrowsException.throwsException(throwableClass, message);
}

/**
* Matcher for {@link Throwable} that expects that the Runnable throws an exception of the provided <code>throwableClass</code> class and has a message matching the provided <code>messageMatcher</code>
*
* @param <T> type of the Runnable
* @param <U> type of the Throwable
* @param throwableClass the Throwable class against which examined exceptions are compared
* @param messageMatcher matcher to validate exception's message
* @return The matcher.
*/
public static <T extends Runnable, U extends Throwable> ThrowsException<T> throwsException(Class<U> throwableClass, Matcher<String> messageMatcher) {
return ThrowsException.throwsException(throwableClass, messageMatcher);
}

/**
* Matcher for {@link Throwable} that expects that the Runnable throws an exception with a message equal to the provided <code>message</code>
*
* @param <T> type of the Runnable
* @param <U> type of the Throwable
* @param message the String against which examined exception messages are compared
* @return The matcher.
*/
public static <T extends Runnable, U extends Throwable> ThrowsException<T> throwsExceptionWithMessage(String message) {
return ThrowsException.throwsExceptionWithMessage(message);
}

/**
* Matcher for {@link Throwable} that expects that the Runnable throws an exception with a message matching the provided <code>messageMatcher</code>
*
* @param <T> type of the Runnable
* @param <U> type of the Throwable
* @param messageMatcher matcher to validate exception's message
* @return The matcher.
*/
public static <T extends Runnable, U extends Throwable> ThrowsException<T> throwsExceptionWithMessage(Matcher<String> messageMatcher) {
return ThrowsException.throwsExceptionWithMessage(messageMatcher);
}
}
80 changes: 80 additions & 0 deletions hamcrest/src/main/java/org/hamcrest/exception/ThrowsException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.hamcrest.exception;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.hamcrest.core.IsInstanceOf;

import static org.hamcrest.core.IsAnything.anything;
import static org.hamcrest.core.IsEqual.equalTo;

/**
* Tests if a Runnable throws a matching exception.
*
* @param <T> the type of the matched Runnable
*/
public class ThrowsException<T extends Runnable> extends TypeSafeDiagnosingMatcher<T> {
private final IsInstanceOf classMatcher;
private final Matcher<? super String> messageMatcher;

public ThrowsException(IsInstanceOf classMatcher, Matcher<? super String> messageMatcher) {
this.classMatcher = classMatcher;
this.messageMatcher = messageMatcher;
}

public static <T extends Runnable> ThrowsException<T> throwsException() {
return throwsException(Throwable.class);
}

public static <T extends Runnable, U extends Throwable> ThrowsException<T> throwsException(Class<U> throwableClass) {
return new ThrowsException<>(new IsInstanceOf(throwableClass), anything("<anything>"));
}

public static <T extends Runnable, U extends Throwable> ThrowsException<T> throwsException(Class<U> throwableClass, String exactMessage) {
return throwsException(throwableClass, equalTo(exactMessage));
}

public static <T extends Runnable, U extends Throwable> ThrowsException<T> throwsException(Class<U> throwableClass, Matcher<String> messageMatcher) {
return new ThrowsException<>(new IsInstanceOf(throwableClass), messageMatcher);
}

public static <T extends Runnable> ThrowsException<T> throwsExceptionWithMessage(String exactMessage) {
return throwsException(Throwable.class, equalTo(exactMessage));
}

public static <T extends Runnable> ThrowsException<T> throwsExceptionWithMessage(Matcher<String> messageMatcher) {
return throwsException(Throwable.class, messageMatcher);
}

@Override
protected boolean matchesSafely(T runnable, Description mismatchDescription) {
try {
runnable.run();
mismatchDescription.appendText("the runnable didn't throw");
return false;
} catch (Throwable t) {
boolean classMatches = classMatcher.matches(t);
if (!classMatches) {
mismatchDescription.appendText("thrown exception class was ").appendText(t.getClass().getName());
}

boolean messageMatches = messageMatcher.matches(t.getMessage());
if (!messageMatches) {
if (!classMatches) {
mismatchDescription.appendText(" and the ");
}
mismatchDescription.appendText("thrown exception message ");
messageMatcher.describeMismatch(t.getMessage(), mismatchDescription);
}

return classMatches && messageMatches;
}
}

@Override
public void describeTo(Description description) {
description
.appendText("a runnable throwing ").appendDescriptionOf(classMatcher)
.appendText(" with message ").appendDescriptionOf(messageMatcher);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.hamcrest.exception;

import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.exception.ThrowsException.throwsException;
import static org.hamcrest.test.MatcherAssertions.*;

public final class ThrowsExceptionTest {

public static void throwIllegalArgumentException() {
throw new IllegalArgumentException("Boom!");
}

public static void throwNullPointerException() {
throw new NullPointerException("Boom!");
}

@Test
public void examples() {
assertThat(ThrowsExceptionTest::throwIllegalArgumentException, throwsException());
assertThat(ThrowsExceptionTest::throwIllegalArgumentException, throwsException(RuntimeException.class));
assertThat(ThrowsExceptionTest::throwIllegalArgumentException, throwsException(RuntimeException.class, "Boom!"));
assertThat(ThrowsExceptionTest::throwIllegalArgumentException, throwsException(RuntimeException.class, containsString("Boo")));
}

@Test
public void evaluatesToTrueIfRunnableThrowsExpectedExceptionWithMatchingMessage() {
assertMatches(
throwsException(IllegalArgumentException.class, "Boom!"),
ThrowsExceptionTest::throwIllegalArgumentException
);

assertDescription(
"a runnable throwing an instance of java.lang.IllegalArgumentException with message \"Boom!\"",
throwsException(IllegalArgumentException.class, "Boom!")
);

assertMismatchDescription(
"thrown exception message was \"Boom!\"",
throwsException(IllegalArgumentException.class, "Bang!"),
(Runnable) ThrowsExceptionTest::throwIllegalArgumentException
);

assertMismatchDescription(
"thrown exception class was java.lang.NullPointerException",
throwsException(IllegalArgumentException.class, "Boom!"),
(Runnable) ThrowsExceptionTest::throwNullPointerException
);

assertMismatchDescription(
"the runnable didn't throw",
throwsException(IllegalArgumentException.class, "Boom!"),
(Runnable) () -> {
}
);
}

@Test
public void evaluatesToTrueIfRunnableThrowsExceptionExtendingTheExpectedExceptionWithMatchingMessage() {
assertMatches(
throwsException(IllegalArgumentException.class, "Boom!"),
ThrowsExceptionTest::throwIllegalArgumentException
);
}

@Test
public void evaluatesToTrueIfRunnableThrowsExceptionWithMatchingMessage() {
assertMatches(
throwsException(IllegalArgumentException.class, containsString("Boo")),
ThrowsExceptionTest::throwIllegalArgumentException
);

assertDescription(
"a runnable throwing an instance of java.lang.IllegalArgumentException with message a string containing \"Boo\"",
throwsException(IllegalArgumentException.class, containsString("Boo"))
);

assertMismatchDescription(
"thrown exception class was java.lang.NullPointerException",
throwsException(IllegalArgumentException.class, containsString("Boo")),
(Runnable) ThrowsExceptionTest::throwNullPointerException
);
}
}

0 comments on commit 1e4230f

Please sign in to comment.