【问题标题】:How to use Hamcrest in Java to test for a exception?如何在 Java 中使用 Hamcrest 来测试异常?
【发布时间】:2014-12-31 18:01:39
【问题描述】:

如何使用 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));      
    }

}

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

虽然有人在 Groovy (How to use Hamcrest to test for exception?) 中问过这个问题,但我的问题是针对用 Java 编写的单元测试。

【问题讨论】:

  • 嘿,COURSE JUnit 没有完善的测试特定操作抛出的方法......缺乏针对失败的函数式编程(即在 Java 8 之前)
  • 你可以看看 assertj 库,它有一个优雅的 api 用于断言异常:joel-costigliola.github.io/assertj/… 示例:assertThatThrownBy(() -> { throw new Exception("boom!"); } ).isInstanceOf(Exception.class);

标签: java junit hamcrest


【解决方案1】:

您真的需要使用Hamcrest 库吗?

如果没有,请参考Junit 对异常测试的支持。 ExpectedException 类有很多方法,除了检查抛出的Exception 的类型之外,您还可以使用它们来做您想做的事情。

您可以结合使用 Hamcrest 匹配器来断言特定的东西,但最好让 Junit 期待抛出的异常。

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=...) 通过抛出给定的异常测试方法执行是否停止, 如果你想抛出异常的调用抛出了一个,则不会。例如,即使doSomething 方法抛出了MyObjectifyNoSuchFieldException 异常,测试也会成功,这可能是不可取的

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

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

【讨论】:

  • 真棒@mystarrocks,谢谢。 @Rule+ExceptedException 方法比 @Test(expected=...) 方法更好吗?
  • @MichaelOsofsky,最好测试一下您认为这次可能抛出异常的确切方法调用。将其与方法级别注释进行对比,该注释不会告诉您哪一行引发了给定的异常。假设我在您怀疑可能抛出给定异常的行之前有一行抛出了相同的异常,那么您的测试实际上应该失败,但不会使用注释方法。 @Test 注释方法真正关心的是方法执行停止抛出给定的异常,而不是哪个调用真正抛出了该异常。
  • 更新了答案,解释了为什么这种方法更好。
  • 另一个好处是可以对异常消息进行断言:exceptionGrabber.expectMessage("My expected message");
  • 它被忽视了,或者我可能误读了一些东西,但你不想在doSomething() 电话之后加一个分号吗?
【解决方案2】:

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

IThrowingRunnable

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

@FunctionalInterface
public interface IThrowingRunnable<E extends Throwable> {

    void run()
            throws E;

}

FailsWithMatcher

这个类实现了一个匹配器,它需要给定的回调来抛出异常。这种实现的一个缺点是,让一个回调抛出一个意外的异常(甚至不抛出一个异常)并不能描述什么是错误的,你会看到完全模糊的错误消息。

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

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

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() 方法的自定义实现可以解决它。

【讨论】:

  • 如果将接口更改为 ...extends Exception,则可以抛出如下错误:完整代码如下:stackoverflow.com/a/47054422/834010
  • 不错。如果“但是:是...”消息会显示抛出的异常,我会对此非常满意。但不幸的是,如果不将状态存储在匹配器中或调用 lambda 两次,我看不到这样做的方法,这通常会产生不同的结果。 :-(
【解决方案3】:

我想最简洁的方法是定义一个类似的函数

public static Throwable exceptionOf(Callable<?> callable) {
    try {
        callable.call();
        return null;
    } catch (Throwable t) {
        return t;
    }
}

某处然后例如打电话

assertThat(exceptionOf(() -> callSomethingThatShouldThrow()),
    instanceOf(TheExpectedException.class));

也许还使用this answer 的ExceptionMessageMatcher 之类的东西。

【讨论】:

    【解决方案4】:

    您应该使用junit-utils,它确实包含一个可以与Hamcrest 的assertThat() 方法一起使用的ExceptionMatcher。

    示例 1:

    assertThat(() -> MyObjectifyNoSuchFieldException.class,
            throwsException(MyObjectifyNoSuchFieldException.class));
    

    示例 2:

    assertThat(() -> myObject.doStuff(null),
            throwsException(MyObjectifyNoSuchFieldException.class)
                .withMessageContaining("ERR-120008"));
    

    更多细节在这里:obvj.net/junit-utils

    【讨论】:

      【解决方案5】:

      从junit 4.13开始你可以使用它的Assert.assertThrows,像这样:

      import static org.junit.Assert.assertThrows;
      
      ...
      
      MyObjectifyNoSuchFieldException ex = assertThrows(MyObjectifyNoSuchFieldException.class, () -> MyObjectifyUtil.getField(DownloadTask.class, fieldName));
      
      // now you can go further and assert things about the exception ex
      // if MyObjectifyUtil.getField(...) does not throw exception, the test will fail right at assertThrows
      

      在我看来,这种异常断言优于@Test(expected=MyObjectifyNoSuchFieldException.class),因为您可以:

      • 进一步断言异常本身;
      • 断言有关副作用的事情(例如在您的模拟中);
      • 继续您的测试用例。

      【讨论】:

        【解决方案6】:

        除上述之外。

        如果将接口更改为 ... extends Exception,则可以抛出如下错误:

        @Override
        protected boolean matchesSafely(final IThrowingRunnable<EX> runnable) {
            try {
                runnable.run();
                throw new Error("Did not throw Exception");
            } catch (final Exception ex) {
                return matcher.matches(ex);
            }
        }
        

        跟踪将如下所示:

        java.lang.Error: Did not throw Exception
            at de.test.test.FailsWithMatcher.matchesSafely(FailsWithMatcher.java:31)
            at de.test.test.FailsWithMatcher.matchesSafely(FailsWithMatcher.java:1)
            at org.hamcrest.TypeSafeMatcher.matches(TypeSafeMatcher.java:65)
            at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:12)
            at org.junit.Assert.assertThat(Assert.java:956)
            at org.junit.Assert.assertThat(Assert.java:923)
            at 
            ...
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-12-07
          • 1970-01-01
          • 2012-08-19
          • 1970-01-01
          • 2018-06-18
          • 1970-01-01
          相关资源
          最近更新 更多