如何测试没有抛出异常?

问题

我知道有一种方法可以做到:

@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's@Rule?)


#1 热门回答(131 赞)

你正以错误的方式接近这一点。只测试你的功能:如果抛出异常,测试将自动失败。如果没有抛出异常,你的测试将全部变为绿色。

我注意到这个问题不时引起人们的兴趣所以我会稍微扩展一下。

#单元测试的背景

当你进行单元测试时,重要的是要自己定义你认为的工作单元。基本上:提取你的代码库,可能包含也可能不包含代表单个功能的多个方法或类。

或者,如第184页的The art of Unit Testing, 2nd Edition by Roy Osherove中所定义:

单元测试是一段自动化的代码,它调用正在测试的工作单元,然后检查有关该单元的单个最终结果的一些假设。单元测试几乎总是使用单元测试框架编写。它可以轻松编写并快速运行。它值得信赖,易读且易于维护。只要生产代码没有改变,它的结果就是一致的。

重要的是要认识到,一个工作单元不仅仅是一种方法,而是在最基本的层面上它是一种方法,之后它被其他工作单元封装。

enter image description here

理想情况下,你应该为每个单独的工作单元都有一个测试方法,这样你就可以随时查看出错的地方。在这个例子中有一个名为getUserById()的基本方法,它将返回一个用户,总共有3个单位的作品。

第一个工作单元应测试在有效和无效输入的情况下是否返回有效用户。
这里必须处理数据源抛出的任何异常:如果没有用户,则应该有一个测试,证明在找不到用户时抛出异常。这个样本可能是IllegalArgumentException,它与@Test(expected = IllegalArgumentException.class)注释有关。

一旦你处理了这个基本工作单元的所有用例,你就提升了一个级别。在这里你做的完全相同,但你只处理来自当前级别下面的级别的异常。这可以使你的测试代码保持良好的结构,并允许你快速浏览架构以找出出错的地方,而不必遍布整个地方。

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

此时应该清楚我们将如何处理这些异常。有两种类型的输入:validinput和defaultininput(输入在严格意义上是有效的,但它不正确)。

当你使用invalidinput时,你正在设置隐含的期望值,即你写的任何测试都会有效。

这样的方法调用可能如下所示:existingUserById_ShouldReturn_UserObject。如果此方法失败(例如:抛出异常),那么你就知道出现了问题,你可以开始挖掘。

通过添加另一个使用thefaultyinput并期望异常的测试(nonExistingUserById_ShouldThrow_IllegalArgumentException),你可以看到你的方法是否完成了错误输入的操作。

#TL; DR

你试图在测试中做两件事:检查输入的有效和错误。通过将其分成两个方法,每个方法都做一件事,你将获得更清晰的测试,并更好地了解出错的地方。

通过记住工作的分层单元,你还可以减少层次结构中较高层的所需测试数量,因为你不必考虑可能在较低层中出错的所有事物:当前层之下的层是虚拟保证,你的依赖项可以工作,如果出现问题,它就在你当前的层中(假设较低层本身不会抛出任何错误)。


#2 热门回答(33 赞)

我偶然发现这是因为SonarQubes规则"squid:S2699":"在这个测试用例中添加至少一个断言。"

我有一个简单的测试那个唯一的目标,它经历了毫无例外。

想象一下这个简单的代码:

public class Printer {

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

可以添加什么样的断言来测试这种方法?当然,你可以试一试它,但这只是代码膨胀。

该解决方案为你提供了JUnit。

如果没有抛出异常,并且你希望明确说明此行为,只需添加预期的内容,如下所示:

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

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


#3 热门回答(25 赞)

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工具:aribitrary代码块和一个简单的注释

//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允许控制流的例外。这是由JRE运行时本身在API(如Double.parseDouble,aNumberFormatExceptionPaths.get,aInvalidPathException)中完成的。

鉴于你已经编写了一个验证Number字符串forDouble.ParseDouble的组件,可能使用了Regex,也许是一个手写的解析器,或者是嵌入了一些其他域规则的东西,这些规则将double的范围限制为特定的,如何最好地测试这个零件?我认为一个明显的测试是断言,当解析结果字符串时,不会抛出任何异常。我会使用上面的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));
}

我还建议你参考input使用TheoriesParameterized参数化此测试,以便你可以更轻松地将此测试重新用于其他输入。或者,如果你想要充满异国情调,你可以选择atest-generation tool(andthis)。 TestNG更好地支持参数化测试。

我发现特别令人不愉快的是建议使用@Test(expectedException=IllegalArgumentException.class),这个异常是危险的广泛.如果你的代码发生变化,导致测试的组件的构造函数为if(constructorArgument <= 0) throw IllegalArgumentException(),并且你的测试为该参数提供了0,因为它很方便 - 这是很常见,因为良好的生成测试数据是一个令人惊讶的难题 - 然后你的测试将是绿色条,即使它没有测试任何东西。这样的测试比没用更糟糕。