题 你怎么断言在JUnit 4测试中抛出某个异常?


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

虽然我当然可以这样做:

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

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

  assertTrue(thrown);
}

我记得有一个注释或Assert.xyz或 某物 对于这些情况来说,JUnit的精神远远不那么笨拙,而且更为精神。


1624
2017-10-01 06:56


起源


任何其他方法的问题,但这是因为一旦抛出异常,它们总是结束测试。另一方面,我经常还想打电话 org.mockito.Mockito.verify 使用各种参数来确保在抛出异常之前发生某些事情(以便使用正确的参数调用记录器服务)。 - ZeroOne
您可以在JUnit wiki页面中查看如何进行异常测试 github.com/junit-team/junit/wiki/Exception-testing - PhoneixS
@ZeroOne - 为此,我将有两个不同的测试 - 一个用于例外,一个用于验证与模拟的交互。 - tddmonkey
有一种方法可以使用JUnit 5,我在下面更新了我的答案。 - Dilini Rajapaksha


答案:


JUnit 4支持这个:

@Test(expected = IndexOutOfBoundsException.class)
public void testIndexOutOfBoundsException() {
    ArrayList emptyList = new ArrayList();
    Object o = emptyList.get(0);
}

参考: https://junit.org/junit4/faq.html#atests_7


1945
2017-10-01 07:12



如果您只希望代码中的某个地方出现异常,而不是像这样的毯子,那么这段代码将无效。 - Oh Chin Boon
@skaffman这不适用于org.junit.experimental.theories.Theory运行org.junit.experimental.theories.Theories - Artem Oboturov
Roy Osherove不鼓励进行这种异常测试 单元测试的艺术因为Exception可能在测试内的任何地方,而不仅仅是在被测单元内部。 - Kevin Wittek
我不同意@ Kiview / Roy Osherove。在我看来,测试应该是行为,而不是实现。通过测试特定方法可能引发错误,您将测试直接绑定到实现。我认为用上面显示的方法进行测试可以提供更有价值的测试。我要补充的警告是,在这种情况下,我会测试一个自定义异常,以便我知道我得到了我真正想要的异常。 - nickbdyer
都不是。我想测试类的行为。重要的是,如果我尝试检索不存在的东西,我会得到一个例外。数据结构的事实 ArrayList 响应 get() 是无关紧要的。如果我将来选择移动到原始数组,那么我将不得不改变这个测试实现。应该隐藏数据结构,以便测试可以关注于行为 类。 - nickbdyer


编辑 现在JUnit5已经发布,最好的选择就是使用 Assertions.assertThrows() (看到 我的另一个答案)。

如果您尚未迁移到JUnit 5,但可以使用JUnit 4.7,则可以使用 ExpectedException 规则:

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()

看到 本文 详情


1166
2018-05-29 17:16



@skaffman - 如果我已经正确理解了这一点,看起来异常只在一个测试中应用了exception.expect,而不是整个类。 - bacar
如果我们期望抛出的异常是一个经过检查的异常,我们应该添加throws或try-catch还是以另一种方式测试这种情况? - MJafar Mash
@MartinTrummer在foo.doStuff()之后不应该运行代码,因为抛出异常并退出该方法。在预期的异常之后有代码(除了在finally中关闭资源之外)无论如何都是无益的,因为如果抛出异常它永远不会被执行。 - Jason Thompson
这是最好的方法。与斯卡弗曼的解决方案相比,这里有两个优点。首先是 ExpectedException class有匹配异常消息的方法,甚至可以编写依赖于异常类的自己的匹配器。其次,您可以在您希望抛出异常的代码行之前设置您的期望 - 这意味着如果错误的代码行抛出异常,您的测试将失败;而对于skaffman的解决方案,没有办法做到这一点。 - Dawood ibn Kareem
@MJafarMash如果检查了您希望抛出的异常,那么您将该异常添加到测试方法的throws子句中。在测试声明抛出已检查异常的方法时,即使未在特定测试用例中触发异常,也会执行相同的操作。 - NamshubWriter


小心使用预期的异常,因为它只断言 方法 抛出那个例外,而不是一个 特定的代码行 在测试中。

我倾向于使用它来测试参数验证,因为这些方法通常非常简单,但更复杂的测试可能更适合:

try {
    methodThatShouldThrow();
    fail( "My method didn't throw when I expected it to" );
} catch (MyException expectedException) {
}

申请判决。


409
2017-10-01 09:31



也许我老了,但我还是喜欢这个。它还为我提供了一个测试异常本身的地方:有时我对某些值的getter有异常,或者我可能只是在消息中查找特定值(例如在消息“unrecognized code'xyz'中查找”xyz“ “)。 - Rodney Gitzel
我认为NamshubWriter的方法可以为您提供两全其美的体验。 - Eddie
+1在某些情况下有用,其中expected = xx与要求不匹配。 - Oh Chin Boon
使用ExpectedException,你可以调用N exception.expect per方法来测试这个exception.expect(IndexOutOfBoundsException.class); foo.doStuff1(); exception.expect(IndexOutOfBoundsException.class); foo.doStuff2(); exception.expect(IndexOutOfBoundsException.class); foo.doStuff3(); - user1154664
@ user1154664其实,你做不到。使用ExpectedException,您只能测试一个方法是否抛出异常,因为当调用该方法时,测试将停止执行,因为它抛出了预期的异常! - NamshubWriter


如前所述,有许多方法可以处理JUnit中的异常。但是对于Java 8,还有另外一个:使用Lambda Expressions。使用Lambda Expressions,我们可以实现如下语法:

@Test
public void verifiesTypeAndMessage() {
    assertThrown(new DummyService()::someMethod)
            .isInstanceOf(RuntimeException.class)
            .hasMessage("Runtime exception occurred")
            .hasMessageStartingWith("Runtime")
            .hasMessageEndingWith("occurred")
            .hasMessageContaining("exception")
            .hasNoCause();
}

assertThrown接受一个功能接口,其实例可以使用lambda表达式,方法引用或构造函数引用创建。 assertThrown接受该接口将期望并准备好处理异常。

这是相对简单但功能强大的技术。

看看这篇描述这种技术的博客文章: http://blog.codeleak.pl/2014/07/junit-testing-exception-with-java-8-and-lambda-expressions.html

源代码可以在这里找到: https://github.com/kolorobot/unit-testing-demo/tree/master/src/test/java/com/github/kolorobot/exceptions/java8

披露:我是博客和项目的作者。


189
2017-07-07 22:35



我喜欢这个解决方案,但我可以从maven回购下载吗? - Selwyn
@Airduster在Maven上可以实现这个想法的一个实现 stefanbirkner.github.io/vallado - NamshubWriter
他们应该将此作为拉取请求提交给JUnit ... - Cristiano Fontes
@CristianoFontes这个API的更简单版本是针对JUnit 4.13的。看到 github.com/junit-team/junit/commit/... - NamshubWriter
@RafalBorowiec技术上, new DummyService()::someMethod 是一个 MethodHandle,但这种方法与lambda表达式同样有效。 - Andy


在junit中,有四种方法可以测试异常。

  • 对于junit4.x,使用Test annonation的可选'expected'属性

    @Test(expected = IndexOutOfBoundsException.class)
    public void testFooThrowsIndexOutOfBoundsException() {
        foo.doStuff();
    }
    
  • 对于junit4.x,请使用ExpectedException规则

    public class XxxTest {
        @Rule
        public ExpectedException thrown = ExpectedException.none();
    
        @Test
        public void testFooThrowsIndexOutOfBoundsException() {
            thrown.expect(IndexOutOfBoundsException.class)
            //you can test the exception message like
            thrown.expectMessage("expected messages");
            foo.doStuff();
        }
    }
    
  • 你也可以使用在junit 3框架下广泛使用的经典try / catch方式

    @Test
    public void testFooThrowsIndexOutOfBoundsException() {
        try {
            foo.doStuff();
            fail("expected exception was not occured.");
        } catch(IndexOutOfBoundsException e) {
            //if execution reaches here, 
            //it indicates this exception was occured.
            //so we need not handle it.
        }
    }
    
  • 最后,对于junit5.x,您还可以使用assertThrows,如下所示

    @Test
    public void testFooThrowsIndexOutOfBoundsException() {
        Throwable exception = assertThrows(IndexOutOfBoundsException.class, () -> foo.doStuff());
        assertEquals("expected messages", exception.getMessage());
    }
    
  • 所以

    • 当您只想测试异常类型时,使用第一种方法
    • 当您想要进一步测试异常消息时,将使用其他三种方法
    • 如果你使用junit 3,那么第三个是首选
    • 如果你喜欢junit 5,那么你应该喜欢第4个
  • 欲了解更多信息,您可以阅读 这个文件 和 junit5用户指南 详情。


84
2017-08-05 08:05



对我来说这是最好的答案,它非常清楚地涵盖了所有方面,谢谢!我个人继续使用第三个选项,即使是Junit4的可读性,为了避免空的catch块,你也可以捕获Throwable并断言e类型 - Nicolas Cornette
是否可以使用ExpectedException来预期检查异常? - miuser
它只是前三个答案的积累。国际海事组织,如果没有增加任何新内容,甚至不应该发布这个答案。只是为代表回答(一个流行的问题)。相当无用。 - Paul Samsotha
当然,因为你可以传递任何派生的类型 Trowable 方法 ExpectedException.expect。请参见 这是签名。 @miuser - walsh


TL;博士

  • JDK8之前:我会推荐旧的好东西 try - catch 块。

  • post-JDK8:使用AssertJ或自定义lambdas来断言 优秀 行为。

无论是Junit 4还是JUnit 5。

长话故事

有可能自己写一个 自己做  try - catch 阻止或使用JUnit工具(@Test(expected = ...) 或者 @Rule ExpectedException JUnit规则功能)。

但这些方式并不那么优雅,不能很好地混合 可读性明智 与其他工具。

  1. try - catch 阻止你必须围绕测试行为编写块,并在catch块中写下断言,这可能没什么问题,但很多人发现这种样式会中断测试的读取流程。你还需要写一个 Assert.fail 在结束时 try 否则测试可能会错过断言的一面; PMDFindBugs的 要么 声纳 会发现这些问题。

  2. @Test(expected = ...) 功能很有意思,因为你可以编写更少的代码然后编写这个测试应该不太容易编码错误。  这种方法缺乏一些领域。

    • 如果测试需要检查异常上的其他内容,例如原因或消息(良好的异常消息非常重要,那么具有精确的异常类型可能还不够)。
    • 此外,由于期望放在方法中,取决于测试代码的编写方式,然后测试代码的错误部分可能抛出异常,导致误报测试,我不确定 PMDFindBugs的 要么 声纳 将提供有关此类代码的提示。

      @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
      }
      
  3. 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,它也会破坏您的读取流程。

    也看到这个 评论 关于作者JUnit的问题 ExpectedException

所以这些上面的选项都有他们的注意事项,显然不能免于编码器错误。

  1. 在创建了这个看起来很有希望的答案后,我发现了一个项目 捕获的异常

    正如项目描述所说的那样,它让编码器用流畅的代码行编写代码来捕获异常并为以后的断言提供此异常。你可以使用任何断言库 Hamcrest 要么 AssertJ

    从主页获取的一个快速示例:

    // 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();
    

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

    目前这个库有两个缺点:

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

    • 它需要另一个测试依赖。

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

    考虑到如果你不想使用catch-exception工具,我将推荐旧的好方法 try - catch 阻止,至少到JDK7。对于JDK 8用户,您可能更喜欢使用AssertJ,因为它提供的可能不仅仅是声明异常。

  2. 使用JDK8,lambdas进入测试场景,并且它们被证明是一种有趣的方式来断言异常行为。 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");
    }
    
  3. 几乎完全重写了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这样的链接断言。

    如果你还记得名字冲突 Matcher 要么 Assert,准备迎接同样的冲突 Assertions

我今天想结束(2017-03-03) AssertJ易于使用,可发现的API,快速的开发速度和作为一个 事实上 对于JDK8,测试依赖是最好的解决方案,无论测试框架如何(JUnit与否),以前的JDK应该依赖于 try - catch 即使他们感到笨拙也会阻止。

这个答案已被复制 另一个问题 我没有相同的知名度,我是同一个作者。


58
2017-12-07 14:19



添加org.junit.jupiter:junit-jupiter-engine:5.0.0-RC2依赖(除了已经存在的junit:junit:4.12)能够使用assertThrows可能不是首选解决方案,但没有引起任何问题给我的问题。 - anre
我是使用ExpectedException规则的粉丝,但它总是困扰我,它打破了AAA。你写了一篇很好的文章来描述所有不同的方法,你肯定鼓励我尝试AssertJ :-)谢谢! - Pim Hazebroek
@PimHazebroek谢谢。 AssertJ API非常丰富。在我看来,JUnit开箱即用的建议更好。 - Brice


BDD 款式方案: JUnit 4 + 捕获例外 + AssertJ

@Test
public void testFooThrowsIndexOutOfBoundsException() {

    when(foo).doStuff();

    then(caughtException()).isInstanceOf(IndexOutOfBoundsException.class);

}

源代码

依赖

eu.codearte.catch-exception:catch-exception:1.3.3

31
2017-11-15 19:17





怎么样:捕获一个非常一般的异常,确保它使它脱离catch块,然后断言异常的类是你期望的那样。如果a)异常是错误的类型(例如,如果你有一个Null指针)而b)没有抛出异常,那么这个断言将失败。

public void testFooThrowsIndexOutOfBoundsException() {
  Throwable e = null;

  try {
    foo.doStuff();
  } catch (Throwable ex) {
    e = ex;
  }

  assertTrue(e instanceof IndexOutOfBoundsException);
}

30
2017-10-01 07:03



这就是你在JUnit 3中所做的.Junit 4做得更好。 - skaffman
此外,当测试失败的那一天,您将看不到测试结果中的Exception ex是什么类型。 - jontejj


用一个 AssertJ 断言,可以与JUnit一起使用:

import static org.assertj.core.api.Assertions.*;

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

  assertThatThrownBy(() -> foo.doStuff())
        .isInstanceOf(IndexOutOfBoundsException.class);
}

它比 @Test(expected=IndexOutOfBoundsException.class)因为它保证测试中的预期行抛出异常并让您检查有关异常的更多详细信息,例如消息,更容易:

assertThatThrownBy(() ->
       {
         throw new Exception("boom!");
       })
    .isInstanceOf(Exception.class)
    .hasMessageContaining("boom");

Maven / Gradle说明在这里。


30
2018-03-05 11:07



最简洁的方式,没有人欣赏它,奇怪..我只有一个问题与assertJ库,assertThat冲突名称与junit的。关于assertJ throwby的更多信息: JUnit:使用Java 8和AssertJ 3.0.0~Codeleak.pl测试异常 - ycomp
@ycomp这是一个非常古老的问题的新答案,所以得分差异具有欺骗性。 - weston
如果可以使用Java 8和AssertJ,这可能是最好的解决方案! - Pierre Henry
@ycomp我怀疑这个名称冲突可能是设计的:因此AssertJ库强烈鼓励你永远不要使用JUnit assertThat,总是AssertJ之一。此外,JUnit方法仅返回“常规”类型,而AssertJ方法返回一个 AbstractAssert 子类...允许如上所述的方法的字符串(或者无论技术术语是什么......)。 - mike rodent
@weston实际上我刚刚在AssertJ 2.0.0中使用了你的技术。毫无疑问,没有理由不升级,但你可能想知道。 - mike rodent


为了解决同样的问题,我确实设置了一个小项目: http://code.google.com/p/catch-exception/

使用这个小助手你会写

verifyException(foo, IndexOutOfBoundsException.class).doStuff();

这比JUnit 4.7的ExpectedException规则简洁得多。 与skaffman提供的解决方案相比,您可以指定您期望异常的代码行。我希望这有帮助。


29
2017-10-28 09:26



我也考虑过这样做,但最终发现ExpectedException的真正威力在于,您不仅可以指定预期的异常,还可以指定异常的某些属性,例如预期的原因或预期的消息。 - Jason Thompson
你也可以这样做 caughtException - Martin Trummer
我的猜测是这个解决方案有一些与模拟相同的缺点?例如,如果 foo 是 final 它会失败,因为你无法代理 foo? - Tom
Tom,如果doStuff()是接口的一部分,代理方法将起作用。否则这种方法会失败,你是对的。 - rwitzel