一尘不染

您如何断言在 JUnit 4 测试中引发了某个异常?

javascript

如何以惯用方式使用 JUnit4 来测试某些代码是否引发异常?

虽然我当然可以做这样的事情:

@Test
public void testFooThrowsIndexOutOfBoundsException() {
  boolean thrown = false;

  try {
    foo.doStuff();
  } catch (IndexOutOfBoundsException e) {
    thrown = true;
  }

  assertTrue(thrown);
}

我记得有一个注解或一个 Assert.xyz 或一些在这种情况下不那么笨拙和更符合 JUnit 精神的东西。


阅读 165

收藏
2022-02-10

共2个答案

一尘不染

编辑:现在 JUnit 5 和 JUnit 4.13 已经发布,最好的选择是使用Assertions.assertThrows() (对于 JUnit 5)和Assert.assertThrows()(对于 JUnit 4.13+)。

如果您还没有迁移到 JUnit 5,但可以使用 JUnit 4.7,则可以使用ExpectedExceptionRule:

public class FooTest {
  @Rule
  public final ExpectedException exception = ExpectedException.none();

  @Test
  public void doStuffThrowsIndexOutOfBoundsException() {
    Foo foo = new Foo();

    exception.expect(IndexOutOfBoundsException.class);
    foo.doStuff();
  }
}

这比因为如果之前抛出@Test(expected=IndexOutOfBoundsException.class)测试将失败要好得多IndexOutOfBoundsException``foo.doStuff()

2022-02-10
一尘不染

  • post-JDK8 :使用AssertJ或自定义 lambda 断言异常行为。
  • pre-JDK8 :我会推荐旧的好try-catch块。(不要忘记在块fail()之前添加一个断言catch

不管是 Junit 4 还是 JUnit 5。

长篇大论

可以自己编写一个自己动手 try-catch阻止或使用 JUnit 工具(@Test(expected = ...)@Rule ExpectedExceptionJUnit 规则功能)。

但是这些方法并不那么优雅,并且不能很好地将可读性与其他工具混合。此外,JUnit 工具确实存在一些缺陷。

  1. -块您必须围绕测试trycatch行为编写块并在 catch 块中写入断言,这可能很好,但许多人发现这种样式会中断测试的读取流程。此外,您需要Assert.fail在块的末尾写一个try。否则,测试可能会错过断言的一侧;PMDfindbugsSonar会发现此类问题。

  2. @Test(expected = ...)功能很有趣,因为您可以编写更少的代码,然后编写此测试据说不太容易出现编码错误。这种方法在某些领域是缺乏的。

  3. 如果测试需要检查异常的其他内容,例如原因或消息(好的异常消息非常重要,拥有精确的异常类型可能还不够)。

  4. 此外,由于期望被放在方法中,根据测试代码的编写方式,测试代码的错误部分可能会引发异常,导致误报测试,我不确定PMDfindbugsSonar将提供有关此类代码的提示。

     @Test(expected = WantedException.class)
     public void call2_should_throw_a_WantedException__not_call1() {
         // init tested
         tested.call1(); // may throw a WantedException

         // call to be actually tested
         tested.call2(); // the call that is supposed to raise an exception
     }
  1. ExpectedException规则也是尝试修复之前的警告,但是使用起来感觉有点尴尬,因为它使用了期望样式,EasyMock用户非常了解这种样式。这对某些人来说可能很方便,但是如果您遵循行为驱动开发(BDD) 或安排行为断言(AAA) 原则,则该ExpectedException规则将不适合这些写作风格。除此之外,它可能会遇到与方式相同的问题@Test,具体取决于您将期望放在哪里。
   @Rule ExpectedException thrown = ExpectedException.none()

   @Test
   public void call2_should_throw_a_WantedException__not_call1() {
       // expectations
       thrown.expect(WantedException.class);
       thrown.expectMessage("boom");

       // init tested
       tested.call1(); // may throw a WantedException

       // call to be actually tested
       tested.call2(); // the call that is supposed to raise an exception
   }

即使预期的异常放在测试语句之前,如果测试遵循 BDD 或 AAA,它也会破坏您的阅读流程。

另外,请参阅ExpectedException. JUnit 4.13-beta-2甚至弃用了这种机制:

拉取请求 #1519:弃用 ExpectedException

Assert.assertThrows 方法提供了一种更好的方法来验证异常。此外,ExpectedException 的使用在与 TestWatcher 等其他规则一起使用时容易出错,因为在这种情况下规则的顺序很重要。

因此,上述这些选项有很多警告,显然不能避免编码错误。

  1. 在创建这个看起来很有希望的答案后,我意识到了一个项目,它是catch-exception

正如该项目的描述所说,它让编码人员用流畅的代码行编写捕获异常并为后一个断言提供此异常。您可以使用任何断言库,例如HamcrestAssertJ

取自主页的快速示例:

   // given: an empty list
   List myList = new ArrayList();

   // when: we try to get the first element of the list
   when(myList).get(1);

   // then: we expect an IndexOutOfBoundsException
   then(caughtException())
           .isInstanceOf(IndexOutOfBoundsException.class)
           .hasMessage("Index: 1, Size: 0") 
           .hasNoCause();

如您所见,代码非常简单,您在特定行捕获异常,thenAPI 是一个别名,将使用 AssertJ API(类似于 using assertThat(ex).hasNoCause()...)。在某些时候,该项目依赖于 AssertJ 的祖先 FEST-Assert编辑:似乎该项目正在酝酿对 Java 8 Lambdas 的支持。

目前,这个库有两个缺点:

  • 在撰写本文时,值得注意的是该库基于 Mockito 1.x,因为它在幕后创建了测试对象的模拟。由于 Mockito 仍未更新,因此该库无法与最终类或最终方法一起使用。即使它基于当前版本的 Mockito 2,这也需要声明一个全局模拟生成器 ( inline-mock-maker),这可能不是您想要的,因为这个模拟生成器与常规模拟生成器有不同的缺点。
  • 它需要另一个测试依赖项。

一旦库支持 lambda,这些问题将不适用。但是,AssertJ 工具集将复制该功能。

综合考虑如果不想用catch-exception工具的话,我会推荐-block的老好办法trycatch至少到JDK7。对于 JDK 8 用户,您可能更喜欢使用 AssertJ,因为它提供的可能不仅仅是断言异常。

  1. 在 JDK8 中,lambda 进入了测试场景,事实证明它们是一种断言异常行为的有趣方式。AssertJ 已经更新,提供了一个很好的流畅的 API 来断言异常行为。

以及使用AssertJ的示例测试:

   @Test
   public void test_exception_approach_1() {
       ...
       assertThatExceptionOfType(IOException.class)
               .isThrownBy(() -> someBadIOOperation())
               .withMessage("boom!"); 
   }

   @Test
   public void test_exception_approach_2() {
       ...
       assertThatThrownBy(() -> someBadIOOperation())
               .isInstanceOf(Exception.class)
               .hasMessageContaining("boom");
   }

   @Test
   public void test_exception_approach_3() {
       ...
       // when
       Throwable thrown = catchThrowable(() -> someBadIOOperation());

       // then
       assertThat(thrown).isInstanceOf(Exception.class)
                         .hasMessageContaining("boom");
   }
  1. 随着对 JUnit 5 的近乎完整的重写,断言得到了一些改进,它们可能被证明是一种有趣的开箱即用的方式来正确断言异常。但实际上断言 API 还是有点差,外面什么都没有assertThrows
 @Test
 @DisplayName("throws EmptyStackException when peeked")
 void throwsExceptionWhenPeeked() {  Throwable t = assertThrows(EmptyStackException.class, () -> stack.peek());
    Assertions.assertEquals("...", t.getMessage());
   }

正如您所注意到的,assertEquals它仍在返回void,因此不允许像 AssertJ 那样链接断言。

此外,如果您记得名字与Matcheror发生冲突Assert,请准备好与 . 发生相同的冲突Assertions

我想得出结论,今天 (2017-03-03) AssertJ的易用性、可发现的 API、快速的开发速度以及作为事实上的测试依赖项是 JDK8 的最佳解决方案,无论测试框架如何(JUnit与否),以前的 JDK 应该改为依赖try-catch块,即使它们感觉很笨重。

2022-02-10