如何以惯用方式使用JUnit4来测试某些代码是否会抛出异常?
虽然我当然可以这样做:
@Test public void testFooThrowsIndexOutOfBoundsException() { boolean thrown = false; try { foo.doStuff(); } catch (IndexOutOfBoundsException e) { thrown = true; } assertTrue(thrown); }
我记得有一个注释或一个Assert.xyz或者其他东西,对于这些类型的情况来说,远不如KUndgy 和JUnit的精神.
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
编辑现在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()
看到 详细信息,此文
小心使用预期的异常,因为它只断言该方法抛出该异常,而不是测试中的特定代码行.
我倾向于使用它来测试参数验证,因为这些方法通常非常简单,但更复杂的测试可能更适合:
try { methodThatShouldThrow(); fail( "My method didn't throw when I expected it to" ); } catch (MyException expectedException) { }
运用判断.
如前所述,有许多方法可以处理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
披露:我是博客和项目的作者.
在junit中,有四种方法可以测试异常.
对于junit4.x,使用Test annonation的可选'expected'属性
@Test public void testFooThrowsIndexOutOfBoundsException() { Throwable exception = assertThrows(IndexOutOfBoundsException.class, () -> foo.doStuff()); assertEquals("expected messages", exception.getMessage()); }
对于junit4.x,请使用ExpectedException规则
@Test(expected = IndexOutOfBoundsException.class) public void testFooThrowsIndexOutOfBoundsException() { foo.doStuff(); }
你也可以使用在junit 3框架下广泛使用的经典try/catch方式
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(); } }
最后,对于junit5.x,您还可以使用assertThrows,如下所示
@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. } }
所以
当您只想测试异常类型时,使用第一种方法
当您想要进一步测试异常消息时,将使用其他三种方法
如果你使用junit 3,那么第三个是首选
如果你喜欢junit 5,那么你应该喜欢第4个
有关详细信息,请参阅此文档和junit5用户指南以获取详细信息.
TL;博士
预JDK8:我会建议老好try
- catch
块.(不要忘记fail()
在catch
块之前添加断言)
post-JDK8:使用AssertJ或自定义lambda来断言异常行为.
无论是Junit 4还是JUnit 5.
长话故事
这是可能自己写一个自己做的 try
- catch
块或使用JUnit工具(@Test(expected = ...)
或@Rule ExpectedException
JUnit的规则功能).
但是这些方式并不那么优雅,并且不能将可读性与其他工具完美地结合在一起.
在try
- catch
块,你必须写周围的测试行为块,并写在catch块的断言,这可能是罚款,但很多人发现taht这种风格中断测试的阅读流程.你还需要Assert.fail
在try
块的末尾写一个,否则测试可能会错过断言的一面; PMD,findbugs或Sonar将发现此类问题.
该@Test(expected = ...)
功能很有趣,因为您可以编写更少的代码,然后编写此测试可能不太容易出现编码错误.但这种做法缺乏一些领域.
如果测试需要检查异常上的其他内容,例如原因或消息(良好的异常消息非常重要,那么具有精确的异常类型可能还不够).
此外,由于期望放在方法中,取决于测试代码的编写方式,然后测试代码的错误部分可能抛出异常,导致误报测试,我不确定PMD,findbugs或Sonar会给出这些代码的提示.
@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 }
该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
.JUnit 4.13-beta-2甚至不赞成这种机制:
拉请求#1519:不推荐使用ExpectedException
方法Assert.assertThrows为验证异常提供了一种更好的方法.此外,与TestWatcher等其他规则一起使用时,ExpectedException的使用容易出错,因为在这种情况下规则的顺序很重要.
所以这些上面的选项都有他们的注意事项,显然不能免于编码器错误.
在创建了这个看起来很有前途的答案后,我发现了一个项目,这是一个例外.
正如项目描述所说,它让编码器用流畅的代码行编写代码来捕获异常并为以后的断言提供此异常.您可以使用任何断言库,如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
block 的旧方法,至少是JDK7.对于JDK 8用户,您可能更喜欢使用AssertJ,因为它提供的可能不仅仅是声明异常.
使用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"); }
通过几乎完全重写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
即使他们感觉笨拙的块.
这个答案是从另一个没有相同可见性的问题复制而来的,我是同一个作者.
现在JUnit 5已经发布,最好的选择是使用Assertions.assertThrows()
(参见Junit 5用户指南).
下面是一个验证抛出异常的示例,并使用Truth对异常消息进行断言:
public class FooTest { @Test public void doStuffThrowsIndexOutOfBoundsException() { Foo foo = new Foo(); IndexOutOfBoundsException e = assertThrows( IndexOutOfBoundsException.class, foo::doStuff); assertThat(e).hasMessageThat().contains("woops!"); } }
其他答案中的方法优势是:
内置于JUnit中
如果lambda中的代码没有抛出异常,则会得到一个有用的异常消息,如果它抛出另一个异常,则会得到一个栈跟踪
简洁
允许您的测试遵循Arrange-Act-Assert
您可以准确地指出您希望抛出异常的代码
您不需要在throws
子句中列出预期的异常
您可以使用您选择的断言框架来对捕获的异常进行断言
org.junit Assert
JUnit 4.13 中将添加类似的方法.
怎么样:捕获一个非常一般的异常,确保它使它脱离catch块,然后断言异常的类是你期望的那样.如果a)异常是错误的类型(例如,如果你有一个Null指针)而b)没有抛出异常,那么这个断言将失败.
public void testFooThrowsIndexOutOfBoundsException() { Throwable e = null; try { foo.doStuff(); } catch (Throwable ex) { e = ex; } assertTrue(e instanceof IndexOutOfBoundsException); }
import static com.googlecode.catchexception.apis.BDDCatchException.*; @Test public void testFooThrowsIndexOutOfBoundsException() { when(() -> foo.doStuff()); then(caughtException()).isInstanceOf(IndexOutOfBoundsException.class); }
https://gist.github.com/mariuszs/23f4e1e1857c28449b61
eu.codearte.catch-exception:catch-exception:2.0
使用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说明在这里.
为了解决同样的问题,我设置了一个小项目:http: //code.google.com/p/catch-exception/
使用这个小助手你会写
verifyException(foo, IndexOutOfBoundsException.class).doStuff();
这比JUnit 4.7的ExpectedException规则简洁得多.与skaffman提供的解决方案相比,您可以指定您期望异常的代码行.我希望这有帮助.
更新: JUnit5对异常测试有一个改进:assertThrows
.
以下示例来自:Junit 5用户指南
@Test void exceptionTesting() { Throwable exception = assertThrows(IllegalArgumentException.class, () -> { throw new IllegalArgumentException("a message"); }); assertEquals("a message", exception.getMessage()); }
使用JUnit 4的原始答案.
有几种方法可以测试抛出异常.我还在我的帖子中讨论了以下选项如何使用JUnit编写出色的单元测试
设置expected
参数@Test(expected = FileNotFoundException.class)
.
@Test(expected = FileNotFoundException.class) public void testReadFile() { myClass.readFile("test.txt"); }
运用 try
catch
public void testReadFile() { try { myClass.readFile("test.txt"); fail("Expected a FileNotFoundException to be thrown"); } catch (FileNotFoundException e) { assertThat(e.getMessage(), is("The file test.txt does not exist!")); } }
用ExpectedException
规则测试.
@Rule public ExpectedException thrown = ExpectedException.none(); @Test public void testReadFile() throws FileNotFoundException { thrown.expect(FileNotFoundException.class); thrown.expectMessage(startsWith("The file test.txt")); myClass.readFile("test.txt"); }
您可以在JUnit4 wiki中阅读有关异常测试和bad.robot的异常测试的更多信息 - 期待异常JUnit规则.
你也可以这样做:
@Test public void testFooThrowsIndexOutOfBoundsException() { try { foo.doStuff(); assert false; } catch (IndexOutOfBoundsException e) { assert true; } }
恕我直言,在JUnit中检查异常的最佳方法是try/catch/fail/assert模式:
// this try block should be as small as possible, // as you want to make sure you only catch exceptions from your code try { sut.doThing(); fail(); // fail if this does not throw any exception } catch(MyException e) { // only catch the exception you expect, // otherwise you may catch an exception for a dependency unexpectedly // a strong assertion on the message, // in case the exception comes from anywhere an unexpected line of code, // especially important if your checking IllegalArgumentExceptions assertEquals("the message I get", e.getMessage()); }
在assertTrue
可能会有点强对某些人来说,这样assertThat(e.getMessage(), containsString("the message");
可能是可取的.
@Test void testFooThrowsIndexOutOfBoundsException() { Throwable exception = expectThrows( IndexOutOfBoundsException.class, foo::doStuff ); assertEquals( "some message", exception.getMessage() ); }
有关JUnit 5的更多信息,请访问http://junit.org/junit5/docs/current/user-guide/#writing-tests-assertions
我在这里尝试了很多方法,但它们要么很复杂,要么完全不符合我的要求.实际上,人们可以非常简单地编写辅助方法:
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; 并且在最后的课程中工作得很好.
我在Mkyong博客中找到的Junit 4最灵活,最优雅的答案.它具有try/catch
使用@Rule
注释的灵活性.我喜欢这种方法,因为您可以读取自定义异常的特定属性.
package com.mkyong; import com.mkyong.examples.CustomerService; import com.mkyong.examples.exception.NameNotFoundException; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.hasProperty; public class Exception3Test { @Rule public ExpectedException thrown = ExpectedException.none(); @Test public void testNameNotFoundException() throws NameNotFoundException { //test specific type of exception thrown.expect(NameNotFoundException.class); //test message thrown.expectMessage(is("Name is empty!")); //test detail thrown.expect(hasProperty("errCode")); //make sure getters n setters are defined. thrown.expect(hasProperty("errCode", is(666))); CustomerService cust = new CustomerService(); cust.findByName(""); } }
如果您想要一个解决方案:
利用Java 8 lambdas
难道不依赖于任何JUnit的魔法
允许您在单个测试方法中检查多个异常
检查测试方法中特定行集引发的异常,而不是整个测试方法中的任何未知行
产生抛出的实际异常对象,以便您可以进一步检查它
这是我写的一个实用函数:
public finalT expectException( Class exceptionClass, Runnable runnable ) { try { runnable.run(); } catch( Throwable throwable ) { if( throwable instanceof AssertionError && throwable.getCause() != null ) throwable = throwable.getCause(); //allows "assert x != null : new IllegalArgumentException();" assert exceptionClass.isInstance( throwable ) : throwable; //exception of the wrong kind was thrown. assert throwable.getClass() == exceptionClass : throwable; //exception thrown was a subclass, but not the exact class, expected. @SuppressWarnings( "unchecked" ) T result = (T)throwable; return result; } assert false; //expected exception was not thrown. return null; //to keep the compiler happy. }
(摘自我的博客)
使用方法如下:
@Test public void testThrows() { RuntimeException e = expectException( RuntimeException.class, () -> { throw new RuntimeException( "fail!" ); } ); assert e.getMessage().equals( "fail!" ); }
JUnit具有内置的支持,具有"预期"属性
在我的情况下,我总是从db获取RuntimeException,但消息不同.并且需要分别处理异常.这是我测试它的方式:
@Test public void testThrowsExceptionWhenWrongSku() { // Given String articleSimpleSku = "999-999"; int amountOfTransactions = 1; Exception exception = null; // When try { createNInboundTransactionsForSku(amountOfTransactions, articleSimpleSku); } catch (RuntimeException e) { exception = e; } // Then shouldValidateThrowsExceptionWithMessage(exception, MESSAGE_NON_EXISTENT_SKU); } private void shouldValidateThrowsExceptionWithMessage(final Exception e, final String message) { assertNotNull(e); assertTrue(e.getMessage().contains(message)); }
只需制作一个可以关闭和打开的匹配器,如下所示:
public class ExceptionMatcher extends BaseMatcher{ private boolean active = true; private Class extends Throwable> throwable; public ExceptionMatcher(Class extends Throwable> throwable) { this.throwable = throwable; } public void on() { this.active = true; } public void off() { this.active = false; } @Override public boolean matches(Object object) { return active && throwable.isAssignableFrom(object.getClass()); } @Override public void describeTo(Description description) { description.appendText("not the covered exception type"); } }
要使用它:
添加public ExpectedException exception = ExpectedException.none();
,然后:
ExceptionMatcher exMatch = new ExceptionMatcher(MyException.class); exception.expect(exMatch); someObject.somethingThatThrowsMyException(); exMatch.off();
在必须返回异常的方法之后,我们可以使用断言失败:
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()); }
在JUnit 4或更高版本中,您可以按如下方式测试异常
@Rule public ExpectedException exceptions = ExpectedException.none();
这提供了许多可用于改进JUnit测试的功能.
如果您看到以下示例,我将在异常上测试3件事.
抛出的异常类型
消息例外
异常的原因
public class MyTest { @Rule public ExpectedException exceptions = ExpectedException.none(); ClassUnderTest classUnderTest; @Before public void setUp() throws Exception { classUnderTest = new ClassUnderTest(); } @Test public void testAppleisSweetAndRed() throws Exception { exceptions.expect(Exception.class); exceptions.expectMessage("this is the exception message"); exceptions.expectCause(Matchers.equalTo(exceptionCause)); classUnderTest.methodUnderTest("param1", "param2"); } }