如何使用JUnit来测试某些代码是否抛出异常?

我当然可以这样做:

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

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

  assertTrue(thrown);
}

我记得,对于这类情况,有一个注释或Assert.xyz之类的东西远没有JUnit那么笨拙,更符合JUnit的精神。


当前回答

我在这里尝试了许多方法,但它们要么很复杂,要么不太符合我的要求。事实上,可以非常简单地编写一个助手方法:

public class ExceptionAssertions {
    public static void assertException(BlastContainer blastContainer ) {
        boolean caughtException = false;
        try {
            blastContainer.test();
        } catch( Exception e ) {
            caughtException = true;
        }
        if( !caughtException ) {
            throw new AssertionFailedError("exception expected to be thrown, but was not");
        }
    }
    public static interface BlastContainer {
        public void test() throws Exception;
    }
}

这样使用:

assertException(new BlastContainer() {
    @Override
    public void test() throws Exception {
        doSomethingThatShouldExceptHere();
    }
});

零依赖:无需mockito,无需powermock;在期末考试中表现很好。

其他回答

我使用Java 8 lambdas的解决方案:

public static <T extends Throwable> T assertThrows(Class<T> expected, ThrowingRunnable action) throws Throwable {
    try {
        action.run();
        Assert.fail("Did not throw expected " + expected.getSimpleName());
        return null; // never actually
    } catch (Throwable actual) {
        if (!expected.isAssignableFrom(actual.getClass())) { // runtime '!(actual instanceof expected)'
            System.err.println("Threw " + actual.getClass().getSimpleName() 
                               + ", which is not a subtype of expected " 
                               + expected.getSimpleName());
            throw actual; // throw the unexpected Throwable for maximum transparency
        } else {
            return (T) actual; // return the expected Throwable for further examination
        }
    }
}

您必须定义FunctionalInterface,因为Runnable没有声明所需的抛出。

@FunctionalInterface
public interface ThrowingRunnable {
    void run() throws Throwable;
}

方法如下:

class CustomException extends Exception {
    public final String message;
    public CustomException(final String message) { this.message = message;}
}
CustomException e = assertThrows(CustomException.class, () -> {
    throw new CustomException("Lorem Ipsum");
});
assertEquals("Lorem Ipsum", e.message);

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

我倾向于将其用于测试参数验证,因为这样的方法通常非常简单,但更复杂的测试可能更好:

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

应用判断。

编辑:现在JUnit 5和JUnit 4.13已经发布,最好的选择是使用Assertions.assertThrows()(针对JUnit 5)和Assertions.AssertThrow()(对于JUnit 4.13+)。有关详细信息,请参阅我的其他答案。

如果您尚未迁移到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(预期=IndexOutOfBoundsException.class)好得多,因为如果在foo.doStuff()之前抛出IndexOutofBoundsExcept,测试将失败

有关详细信息,请参阅本文。

tl;博士

JDK8后:使用AssertJ或自定义lambdas断言异常行为。JDK8之前的版本:我将推荐旧的好的try-catch块。(不要忘记在catch块之前添加fail()断言)

无论Junit 4还是Junit 5。

长话短说

可以自己编写一个自己动手的try-catch块或使用JUnit工具(@Test(expected=…)或@Rule ExpectedException JUnit规则特性)。

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

try-catch块必须围绕测试行为编写块,并在catch块中写入断言,这可能很好,但许多人发现这种样式会中断测试的读取流程。此外,您需要在try块的末尾编写Assert.fail。否则,测试可能会漏掉断言的一面;PMD、findbugs或Sonar将发现此类问题。@Test(expected=…)特性很有趣,因为你可以编写更少的代码,然后编写这个测试就不太容易出现编码错误。但在某些领域缺乏这种方法。如果测试需要检查异常的其他内容,如原因或消息(好的异常消息非常重要,只有精确的异常类型可能不够)。同样,由于在方法中放置了期望值,根据测试代码的编写方式,测试代码的错误部分可能会引发异常,导致测试结果为假阳性,我不确定PMD、findbugs或Sonar是否会给出此类代码的提示。@测试(应为=WantedException.class)public void call2_should_throw_a_WantedException__not_call1(){//初始化测试tested.call1();//可能引发WantedException//待实际测试的呼叫tested.call2();//应该引发异常的调用}ExpectedException规则也是一种修复之前警告的尝试,但使用起来有点尴尬,因为它使用了预期风格,EasyMock用户非常了解这种风格。这对某些人来说可能很方便,但如果你遵循行为驱动开发(BDD)或安排行为断言(AAA)原则,ExpectedException规则将不适合这些写作风格。除此之外,它可能会遇到与@Test方法相同的问题,这取决于您将期望放在何处。@抛出的规则ExpectedException=ExpectedException.none()@测试public void call2_should_throw_a_WantedException__not_call1(){//期望抛出.exexpect(WantedException.class);抛出.expectMessage(“boom”);//初始化测试tested.call1();//可能引发WantedException//待实际测试的呼叫tested.call2();//应该引发异常的调用}即使预期的异常被放置在测试语句之前,如果测试遵循BDD或AAA,它也会破坏您的阅读流程。另外,请参阅ExpectedException作者JUnit上的这个评论问题。JUnit 4.13-beta-2甚至反对这种机制:拉取请求#1519:预期不推荐异常Assert.assertThrows方法为验证异常提供了更好的方法。此外,当与TestWatcher等其他规则一起使用时,ExpectedException的使用容易出错,因为在这种情况下,规则的顺序很重要。

因此,以上这些选项都有其所有的警告,并且显然不能避免编码器错误。

有一个项目是我在创建这个答案后意识到的,看起来很有希望,那就是catch exception。正如项目描述所说,它让程序员编写一行流畅的代码来捕捉异常,并为后一个断言提供这个异常。您可以使用任何断言库,如Hamcrest或AssertJ。主页上的一个快速示例://给定:空列表List myList=新建ArrayList();//当:我们尝试获取列表的第一个元素当(myList).get(1);//那么:我们需要IndexOutOfBoundsException然后(caughtException()).isInstanceOf(IndexOutOfBoundsException.class).hasMessage(“索引:1,大小:0”).hhasNoCause();正如您所看到的,代码非常简单,您在特定的行上捕捉到异常,然后API是一个别名,它将使用AssertJ API(类似于使用assertThat(ex).hasNoCause()…)。在某些时候,该项目依赖于FEST Assert,即AssertJ的祖先。编辑:该项目似乎正在酝酿对Java 8 Lambdas的支持。目前,该库有两个缺点:在撰写本文时,值得注意的是,这个库是基于Mockito 1.x的,因为它在后台创建了一个被测试对象的模拟。由于Mockito仍然没有更新,所以这个库不能与final类或final方法一起使用。即使它是基于当前版本中的Mockito 2,这也需要声明一个全局模拟生成器(内联模拟生成器),这可能不是您想要的,因为这个模拟生成器具有与常规模拟生成器不同的缺点。它还需要另一个测试依赖项。一旦库支持lambdas,这些问题就不适用了。但是,AssertJ工具集将复制该功能。如果您不想使用catch异常工具,请考虑所有这些,我将推荐try-catch块的旧好方法,至少到JDK7为止。对于JDK8用户,您可能更喜欢使用AssertJ,因为它提供的可能不仅仅是断言异常。有了JDK8,lambdas进入了测试场景,事实证明,这是一种很有意思的方式来断言异常行为。AssertJ已经更新,提供了一个非常流畅的API来断言异常行为。以及AssertJ的示例测试:@测试public void test_exception_approach_1(){...assertThatExceptionOfType(IOException.class).isThrownBy(()->someBadIOOperation()).带有消息(“boom!”);}@测试public void test_exception_approach_2(){...assertThatThrownBy(()->someBadIOOperation()).isInstanceOf(异常.class).hasMessageContaining(“boom”);}@测试public void test_exception_approach_3(){...//当Throwable throw=catchThrowable(()->someBadIOOperation());//然后assertThat(引发).isInstanceOf(Exception.class).hasMessageContaining(“boom”);}随着JUnit5的几乎完全重写,断言得到了一些改进,它们可能会被证明是一种开箱即用的正确断言异常的方法。但实际上,断言API仍然有点差,除了assertThrows之外没有其他东西。@测试@DisplayName(“偷看时抛出EmptyStackException”)void throwsExceptionWhenPeeked(){Throwable t=assertThrows(EmptyStackException.class,()->stack.ppeek());断言.assertEquals(“…”,t.getMessage());}正如您所注意到的,assertEquals仍然返回void,因此不允许像AssertJ那样链接断言。此外,如果您记得与Matcher或Assert的名称冲突,请准备好与Assertions的名称冲突。

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

这个答案是从另一个不具有相同可见性的问题中复制的,我是同一个作者。

我们可以在必须返回异常的方法之后使用断言失败:

try{
   methodThatThrowMyException();
   Assert.fail("MyException is not thrown !");
} catch (final Exception exception) {
   // Verify if the thrown exception is instance of MyException, otherwise throws an assert failure
   assertTrue(exception instanceof MyException, "An exception other than MyException is thrown !");
   // In case of verifying the error message
   MyException myException = (MyException) exception;
   assertEquals("EXPECTED ERROR MESSAGE", myException.getMessage());
}