如何在Java中使用Hamcrest来测试异常?

2022-09-02 00:16:28

如何使用 Hamcrest 测试异常?根据 https://code.google.com/p/hamcrest/wiki/Tutorial 中的注释,“异常处理由 Junit 4 使用预期的属性提供。

所以我尝试了这个,发现它的工作原理:

public class MyObjectifyUtilTest {

    @Test
    public void shouldFindFieldByName() throws MyObjectifyNoSuchFieldException {
        String fieldName = "status";
        String field = MyObjectifyUtil.getField(DownloadTask.class, fieldName);
        assertThat(field, equalTo(fieldName));
    }

    @Test(expected=MyObjectifyNoSuchFieldException.class)
    public void shouldThrowExceptionBecauseFieldDoesNotExist() throws MyObjectifyNoSuchFieldException {
        String fieldName = "someMissingField";
        String field = MyObjectifyUtil.getField(DownloadTask.class, fieldName);
        assertThat(field, equalTo(fieldName));      
    }

}

Hamcrest 是否在 JUnit 的注释之外提供了任何其他功能?@Test(expected=...)

虽然有人在Groovy中问过这个问题(如何使用Hamcrest来测试异常?),但我的问题是用Java编写的单元测试。


答案 1

你真的需要使用图书馆吗?Hamcrest

如果没有,下面介绍如何使用 对异常测试的支持来实现。ExpectedException 类有很多方法,除了检查抛出的类型之外,您还可以使用这些方法来执行所需的操作。JunitException

您可以将匹配器与此结合使用来断言特定内容,但最好让预期引发的异常。HamcrestJunit

public class MyObjectifyUtilTest {

    // create a rule for an exception grabber that you can use across 
    // the methods in this test class
    @Rule
    public ExpectedException exceptionGrabber = ExpectedException.none();

    @Test
    public void shouldThrowExceptionBecauseFieldDoesNotExist() throws MyObjectifyNoSuchFieldException {
        String fieldName = "someMissingField";

        // a method capable of throwing MyObjectifyNoSuchFieldException too
        doSomething();

        // assuming the MyObjectifyUtil.getField would throw the exception, 
        // I'm expecting an exception to be thrown just before that method call
        exceptionGrabber.expect(MyObjectifyNoSuchFieldException.class);
        MyObjectifyUtil.getField(DownloadTask.class, fieldName);

        ...
    }

}

这种方法比

  • @Test (expected=...)方法,因为仅测试方法执行是否通过引发给定的异常而停止,而不是测试要引发异常的调用是否抛出一个异常。例如,即使方法抛出了可能不希望的异常,测试也会成功@Test (expected=...)doSomethingMyObjectifyNoSuchFieldException

  • 您可以测试的不仅仅是所引发的异常的类型。例如,您可以检查特定的异常实例或异常消息等

  • 块方法,因为可读性和简洁性。try/catch


答案 2

如果计算断言错误描述,我无法以一种很好的方式实现它(可能这就是为什么Hamcrest没有提供这样的功能),但是如果你玩得很好Java 8,那么你可能需要这样的东西(但是我认为由于下面描述的问题,它不会被使用):

IThrowingRunnable

此接口用于包装可能引发异常的代码。 也可以使用,但后者需要返回一个值,所以我认为一个runnable(“void-callable”)更方便。Callable<E>

@FunctionalInterface
public interface IThrowingRunnable<E extends Throwable> {

    void run()
            throws E;

}

FailWithMatcher

此类实现一个匹配器,该匹配器要求给定的回调引发异常。此实现的缺点是,回调引发意外异常(甚至没有引发单个异常)并不能描述错误所在,并且您会看到完全模糊的错误消息。

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);
    }

}

异常消息匹配器

这是一个示例匹配器,用于对引发的异常消息进行简单检查。

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);
    }

}

以及测试样品本身

@Test
public void test() {
    assertThat(() -> emptyList().get(0), failsWith(IndexOutOfBoundsException.class, exceptionMessage("Index: 0")));
    assertThat(() -> emptyList().set(0, null), failsWith(UnsupportedOperationException.class));
}

请注意,此方法:

  • ...独立于测试运行程序
  • ...允许在单个测试中指定多个断言

最糟糕的是,典型的失败会像这样

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>

也许使用该方法的自定义实现可以修复它。assertThat()


推荐