如何测试代码不会抛出异常?

442

我知道完成它的一种方法是:

@Test
public void foo() {
   try {
      // execute code that you expect not to throw Exceptions.
   } catch(Exception e) {
      fail("Should not have thrown any exception");
   }
}

有没有更简洁的方法来完成这个任务?(可能使用Junit的@Rule注解?)


14
如果JUnit测试抛出除了预期异常以外的任何异常,就会判定测试失败。通常不应该出现任何异常。 - Raedwald
2
在JUnit中,失败和错误之间难道没有区别吗?第一个意味着测试失败,第二个意味着发生了意外情况。 - Victor Basso
2
可能是重复的问题:如何测试特定异常是否未被抛出? - Ciro Santilli OurBigBook.com
@Vituel 是的,在NetBeans中有区别,而且非常清晰。错误是红色的,失败是黄色的。 - Alonso del Arte
@Raedwald 在截止日期的压力下,我可能会这样看待它。但是如果有充足的时间,我不想在红色和绿色之间没有黄色。 - Alonso del Arte
20个回答

299

JUnit 5(Jupiter)提供三个函数来检查异常的存在/缺失:

assertAll​()

断言所有提供的executables
  都不会抛出异常。

assertDoesNotThrow​()

断言执行
  提供的executable/supplier
不会抛出任何类型的异常

  自JUnit 5.2.0(2018年4月29日)起,此函数可用。

assertThrows​()

断言执行提供的executable
将引发expectedType
  类型的异常,并返回exception

示例

package test.mycompany.myapp.mymodule;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;

class MyClassTest {

    @Test
    void when_string_has_been_constructed_then_myFunction_does_not_throw() {
        String myString = "this string has been constructed";
        assertAll(() -> MyClass.myFunction(myString));
    }
    
    @Test
    void when_string_has_been_constructed_then_myFunction_does_not_throw__junit_v520() {
        String myString = "this string has been constructed";
        assertDoesNotThrow(() -> MyClass.myFunction(myString));
    }

    @Test
    void when_string_is_null_then_myFunction_throws_IllegalArgumentException() {
        String myString = null;
        assertThrows(
            IllegalArgumentException.class,
            () -> MyClass.myFunction(myString));
    }

}

20
现在这是最佳答案。其他答案正在讨论旧版本的JUnit。 - Tejesh Raut
1
链接存在问题。 - JobHunter69
这是Spring Boot 2.2.0+和JUnit 5的最佳答案。 - jiangke
4
现在你可以这样做:assertDoesNotThrow(myObject::myValidationFunction); - Alex
1
无瑕疵的答案! - Gaurav

280
您正在错误的方向上接近问题。只需测试您的功能:如果抛出异常,测试将自动失败。如果没有抛出异常,则您的测试将全部变为绿色。
我注意到这个问题会时不时地引起兴趣,所以我会稍微扩展一下。
单元测试背景
当您进行单元测试时,重要的是要定义您认为的工作单元。基本上:从代码库中提取一个可能包含多个方法或类的单个功能块,表示单个功能。
或者,如The art of Unit Testing, 2nd Edition by Roy Osherove第11页所定义:
单元测试是一段自动化代码,它调用正在测试的工作单元,然后检查有关该单元的单个最终结果的某些假设。单元测试几乎总是使用单元测试框架编写的。它可以轻松编写并快速运行。它是值得信赖的、可读的和可维护的。只要生产代码没有更改,它的结果就是一致的。
重要的是要认识到,一个“工作单元”通常不仅仅是一个方法,但在基本层面上它确实是一个方法,之后它被其他“工作单元”所封装。

enter image description here

理想情况下,您应该为每个单独的工作单元编写一个测试方法,以便您始终可以立即查看出错的位置。在此示例中,有一个基本方法称为getUserById(),它将返回一个用户,总共有3个工作单元。
第一个工作单元应该测试在有效和无效输入的情况下是否返回有效用户。
必须在此处处理数据源抛出的任何异常:如果没有用户存在,则应该有一个测试来演示当找不到用户时抛出异常。其中一个示例是使用@Test(expected = IllegalArgumentException.class)注释捕获的IllegalArgumentException
一旦您处理了此基本工作单元的所有用例,就可以向上移动一个级别。在这里,您完全相同,但只处理当前级别下面的异常。这使您的测试代码结构良好,并允许您快速运行体系结构以查找错误位置,而不必跳来跳去。

处理测试的有效和错误输入

在这一点上,我们应该清楚如何处理这些异常。有两种类型的输入:有效输入和错误输入(严格意义上的输入是有效的,但不正确)。
当您使用有效输入时,您正在设置隐含的期望,即无论您编写什么样的测试,都将起作用。
这样的方法调用可能看起来像这样:existingUserById_ShouldReturn_UserObject。如果此方法失败(例如:抛出异常),则您知道出了问题,可以开始挖掘。
通过添加另一个测试(nonExistingUserById_ShouldThrow_IllegalArgumentException),该测试使用错误输入并期望异常,您可以查看您的方法是否按照其所应该处理错误的输入。
简而言之,您试图在测试中做两件事情:检查有效和错误的输入。通过将其拆分为每个执行一项任务的两个方法,您将拥有更清晰的测试和更好的概述,以确定哪些地方出错。
通过牢记工作的分层单元,您还可以减少对较高层次的测试所需的数量,因为您不必考虑在较低层次中可能出现的每件事情:当前层下面的层是您依赖项正常工作的虚拟保证,如果出现问题,那么就在当前层(假设较低层没有自己抛出任何错误)。

7
问题在于我正在尝试使用TDD,而我使用的其中一个协作者正在抛出异常。因此,我需要测试我是否正在消耗协作者抛出的异常。 - Ankit Dhingra
8
你是说你的功能依赖于处理异常吗?这是一种代码异味:异常用于优雅地让您捕获问题,而不是用于控制流程。如果您想测试应该抛出异常的情况,则应使用“expected”注释。如果您想测试代码失败并且想要查看错误是否被正确处理,请使用“expected”并可能使用断言来确定是否已解决该问题。 - Jeroen Vannevel
22
测试引发异常的错误情况是否得到正确处理是完全有效的。 - Thorbjørn Ravn Andersen
1
据我所知,如果您不在可能引发异常的代码周围编写自己的try/catch块,则无法编译java。因此,您被迫执行try {} catch(Exception e) { fail() }。 - user153275
1
@dpk 是的,你可以这样做。你只需在测试中添加 throws IllegalArgumentException 即可。最终你想要的是,如果出现异常,你的测试会变成红色。好吧,你知道吗?你不需要编写 fail()。正如 @Jeroen Vannevel 所写:“如果抛出异常,测试将自动失败”。 - Amedee Van Gasse
显示剩余11条评论

260

我因SonarQube的规则"squid:S2699"偶然发现了这个问题:“在此测试用例中至少添加一个断言。”

我有一个简单的测试,其唯一目的是在不抛出异常的情况下进行。

请考虑以下简单代码:

public class Printer {

    public static void printLine(final String line) {
        System.out.println(line);
    }
}

测试该方法可以添加什么样的断言?虽然你可以在它周围加上try-catch,但那只是代码膨胀。

解决方案来自于JUnit本身。

如果没有抛出异常并且您想明确说明这种行为,请像下面的示例中一样添加expected

@Test(expected = Test.None.class /* no exception expected */)
public void test_printLine() {
    Printer.printLine("line");
}

Test.None.class 是预期值的默认值。

如果您导入了org.junit.Test.None,然后您可以写如下代码:

@Test(expected = None.class)

您可能会发现这更易读。


72
我认为这是最佳答案。被接受的答案很好,作者正确地指出了代码异味。然而,他并没有真正回答具体的问题。 - HellishHeat
6
值得注意的是,预期结果的默认值为None,所以只需在方法上加上@Test注解即可。 - oziomajnr
10
仅仅在方法上添加@Test注解并不能解决SonarQube问题。 - David Conrad
这正是我遇到的完全相同的问题。虽然我完全同意@jeroen-vannevel的答案,但我需要某种验证来确保SonarQube不会引发任何问题。 - Ernani
19
为Junit5添加相同的解决方案:assertDoesNotThrow(() -> Printer.printLine("line")); - Ankush
这个答案是最好的! - Ranch Camal

127

对于JUnit 5之前的版本:

使用AssertJ fluent assertions 3.7.0

Assertions.assertThatCode(() -> toTest.method())
    .doesNotThrowAnyException();

更新:

JUnit 5 引入了 assertDoesNotThrow() 断言,所以我更喜欢使用它而不是向您的项目添加额外的依赖项。有关详细信息,请参见此答案


优美简洁的回答。 - Ermiya Eskandary

34
Java 8使得这项任务变得轻而易举,而Kotlin/Scala则更加方便。 我们可以编写一个小实用工具类。
class MyAssertions{
  public static void assertDoesNotThrow(FailingRunnable action){
    try{
      action.run()
    }
    catch(Exception ex){
      throw new Error("expected action not to throw, but it did!", ex)
    }
  }
}

@FunctionalInterface interface FailingRunnable { void run() throws Exception }

然后你的代码就变得简单了:

@Test
public void foo(){
  MyAssertions.assertDoesNotThrow(() -> {
    //execute code that you expect not to throw Exceptions.
  }
}

如果您没有使用Java-8的权限,我建议使用一个非常古老的Java工具:任意代码块和简单注释。
//setup
Component component = new Component();

//act
configure(component);

//assert 
/*assert does not throw*/{
  component.doSomething();
}

最后,使用Kotlin这门语言,我最近深深爱上了它:

fun (() -> Any?).shouldNotThrow() 
    = try { invoke() } catch (ex : Exception){ throw Error("expected not to throw!", ex) }

@Test fun `when foo happens should not throw`(){

  //...

  { /*code that shouldn't throw*/ }.shouldNotThrow()
}

虽然你可以根据自己的喜好灵活表达,但我一直是流畅断言(Fluent Assertions)的粉丝。


关于

你的方法不对。只需测试你的功能:如果抛出异常,则测试将自动失败。如果没有抛出异常,则所有测试都应该通过。

原则上是正确的,但结论是错误的。

Java允许使用异常控制流程。在像Double.parseDoublePaths.get这样的API中,JRE运行时本身就使用了NumberFormatExceptionInvalidPathException等异常。

假设你已经编写了一个验证Double.ParseDouble数字字符串的组件,可能使用了正则表达式,可能使用了手写解析器,或者可能嵌入了其他限制双精度范围的域规则,如何最好地测试此组件?我认为一个明显的测试是断言当解析结果字符串时不会抛出异常。使用上述assertDoesNotThrow/*comment*/{code}代码块编写此测试,类似于:

@Test public void given_validator_accepts_string_result_should_be_interpretable_by_doubleParseDouble(){
  //setup
  String input = "12.34E+26" //a string double with domain significance

  //act
  boolean isValid = component.validate(input)

  //assert -- using the library 'assertJ', my personal favourite 
  assertThat(isValid).describedAs(input + " was considered valid by component").isTrue();
  assertDoesNotThrow(() -> Double.parseDouble(input));
}

我还建议您使用TheoriesParameterizedinput参数进行参数化,以便您可以更轻松地将此测试用于其他输入。或者,如果您想要更加奇特一些,您可以选择测试生成工具(和这个)。TestNG对参数化测试的支持更好。
我认为特别不赞成使用@Test(expectedException=IllegalArgumentException.class),因为这种异常太过宽泛。如果您的代码发生了变化,使得被测试组件的构造函数有了if(constructorArgument <= 0) throw IllegalArgumentException(),而您的测试为该参数提供了0,因为这很方便——这是非常常见的,因为生成良好的测试数据是一个出奇的难题——那么您的测试将会是绿色的,即使它什么都没有测试到。这样的测试比无用还要糟糕。

2
自JUnit 4.13以来,您可以使用Assert.assertThrows来检查某些代码是否引发异常。 - MageWind

31

如果你很不幸地发现你的代码中有所有的错误,那么你只能愚蠢地做如下操作:

class DumpTest {
    Exception ex;
    @Test
    public void testWhatEver() {
        try {
            thisShouldThrowError();
        } catch (Exception e) {
            ex = e;
        }
        assertEquals(null,ex);
    }
}

1
只是一个小建议,在你测试之前,Exception ex 应该先赋值为 = null; - Denees
4
这不是一个好的解决方案。如果本不应抛出异常的方法确实抛出了异常,你将得不到有用的错误信息。只需调用不应抛出异常的方法,并测试其返回值(或副作用,比如记录异常)。如果以后该方法意外抛出异常,测试将失败。 - NamshubWriter
6
可以在 catch 块中放置 Assert.fail(),这样做更加简单美观。 - isaac.hazan
是的,我同意你的观点。另一种方法是在方法上方添加注释 @Test(expected = InvalidRequestException.class)。 - Ben Tennyson
这对我非常有用,谢谢。在我的情况下,我正在AwaitilityuntilAsserted(ThrowingRunnable assertion)中进行断言。我调用的方法一开始总是会抛出异常,但我想断言它最终会停止这样做(根据Awaitility的参数)。 - Ubeogesh
同意,这是一个丑陋的解决方案,但至少它让SonarLint满意了。 - Jasper Citi

27

虽然这篇文章已经6年了,但是JUnit世界已经发生了很多变化。使用JUnit5,你现在可以使用

org.junit.jupiter.api.Assertions.assertDoesNotThrow()

例:

public void thisMethodDoesNotThrowException(){
   System.out.println("Hello There");
}

@Test
public void test_thisMethodDoesNotThrowException(){
  org.junit.jupiter.api.Assertions.assertDoesNotThrow(
      ()-> thisMethodDoesNotThrowException()
    );
}

希望这能帮助使用Junit5新版本的人们。


我希望有一种方法可以在此指定具体的异常类。我必须在AwaitilityuntilAsserted(ThrowingRunnable assertion)内部执行此操作。目前测试的系统正在我的提供的ThrowingRunnable上抛出特定的异常,但我希望它有一些时间停止这样做。但是,如果它抛出不同的异常,我希望测试立即失败。 - Ubeogesh

8
JUnit5添加了assertAll()方法,正是为了实现这个目的。
assertAll( () -> foo() )

来源:JUnit 5 API


7

测试一个像无返回值方法一样的场景

void testMeWell() throws SomeException {..}

避免抛出异常:

Junit5

assertDoesNotThrow(() -> {
    testMeWell();
});

4

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接