Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement matchers for thrown exceptions in Runnable #423

Merged
merged 16 commits into from
Nov 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
);
}
}