如何使用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);
编辑:现在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块,即使它们觉得笨拙。
这个答案是从另一个不具有相同可见性的问题中复制的,我是同一个作者。