关于java:如何测试没有抛出异常?

How to test that no exception is thrown?

我知道这样做的一个方法是:

1
2
3
4
5
6
7
8
9
@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)


你走错了方向。只需测试您的功能:如果抛出异常,测试将自动失败。如果没有异常,您的测试将全部变为绿色。

我已经注意到这个问题不时引起人们的兴趣,所以我会扩大一点。

单元测试背景

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

或者,如Roy Osherove第二版《单元测试技术》第11页所述:

A unit test is an automated piece of code that invokes the unit of work being tested, and then checks some assumptions about a single end result of that unit. A unit test is almost always written using a unit testing framework. It can be written easily and runs quickly. It's trustworthy, readable, and maintainable. It's consistent in its results as long as production code hasn't changed.

重要的是要认识到,一个工作单元通常不仅仅是一种方法,而且在非常基础的层面上,它是一种方法,然后它被其他工作单元封装。

enter image description here

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

第一个工作单元应该测试在输入有效和无效的情况下是否返回有效用户。数据源引发的任何异常都必须在此处处理:如果不存在用户,则应该有一个测试来证明在找不到用户时引发了异常。其中的一个示例可以是IllegalArgumentException,它是由@Test(expected = IllegalArgumentException.class)注释捕获的。

一旦您处理了这个基本工作单元的所有用例,就可以提升一个级别。在这里,您做的完全相同,但是您只处理来自当前级别正下方级别的异常。这使您的测试代码保持了良好的结构,并允许您快速地运行整个体系结构,以发现哪里出了问题,而不是必须到处跳。

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

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

当您使用有效输入工作时,您将设置隐式期望值,您编写的任何测试都将工作。

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

通过添加另一个使用错误输入并期望出现异常的测试(nonExistingUserById_ShouldThrow_IllegalArgumentException),您可以看到您的方法是否在错误输入下执行它应该执行的操作。

DR

您试图在测试中做两件事:检查有效输入和错误输入。通过将其分为两种方法,每种方法只做一件事,您将有更清晰的测试和对哪里出错的更好的概述。

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


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

我有一个简单的测试,它的唯一目标是通过测试而不抛出异常。

考虑这个简单的代码:

1
2
3
4
5
6
public class Printer {

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

可以添加哪种断言来测试此方法?当然,您可以试着绕过它,但这只是代码膨胀。

解决方案来自JUnit本身。

如果没有异常,并且您希望显式地说明这种行为,只需添加expected,如下例所示:

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

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


Java 8使这更容易,KOTLIN / Scala加倍。

我们可以写一个实用程序类

1
2
3
4
5
6
7
8
9
10
11
12
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 }

然后你的代码变得简单:

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

如果您不能访问Java8,我将使用一个痛苦的旧Java工具:Adple代码块和一个简单的注释。

1
2
3
4
5
6
7
8
9
10
//setup
Component component = new Component();

//act
configure(component);

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

最后,我爱上了Kotlin这门语言:

1
2
3
4
5
6
7
8
9
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()
}

尽管有很多空间来摆弄你到底想表达什么,但我一直是一个流利断言的粉丝。

关于

You're approaching this the wrong way. Just test your functionality: if an exception is thrown the test will automatically fail. If no exception is thrown, your tests will all turn up green.

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

Java允许控制流的异常。这是由jre运行时本身在类似于Double.parseDouble的API中通过NumberFormatExceptionPaths.get通过InvalidPathException完成的。

假设您已经编写了一个组件来验证Double.parseDouble的数字字符串,可能使用regex,也可能是手写的解析器,或者嵌入一些其他域规则,将double的范围限制为特定的范围,那么如何最好地测试这个组件呢?我认为一个显而易见的测试是断言,当解析结果字符串时,不会引发异常。我将使用上面的assertDoesNotThrow/*comment*/{code}块编写该测试。类似的东西

1
2
3
4
5
6
7
8
9
10
11
@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,因为它很方便——这是非常常见的,因为良好的测试数据生成是一个令人惊讶的难题——那么即使您的测试什么也不测试,您的测试也将是绿色条。这样的测试比无用更糟糕。


如果您不走运,无法捕获代码中的所有错误。你可以愚蠢地做

1
2
3
4
5
6
7
8
9
10
11
12
class DumpTest {
    Exception ex;
    @Test
    public void testWhatEver() {
        try {
            thisShouldThroughError();
        } catch (Exception e) {
            ex = e;
        }
        assertEquals(null,ex);
    }
}


使用Assertj Fluent断言3.7.0:

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


Junit 5(Jupiter)提供三个功能来检查异常是否存在:

assertAll?()

&emsp;断言所有提供的executables&不要抛出异常。

assertDoesNotThrow?()

&声明执行&emsp;提供executable/supplier。&emsp;不会引发任何类型的异常。

&emsp;此功能可用&emsp;自6月5.2.0日(2018年4月29日)起。

assertThrows?()

&emsp;断言执行所提供的executable。&抛出expectedType的异常&并返回异常。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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));
    }

}


JUnit5为此目的添加了assertAll()方法。

1
assertAll( () -> foo() )

来源:JUnit 5 API


您可以使用@rule,然后调用方法reportMissingExceptionWithMessage,如下所示:这是斯卡拉,但它可以很容易地在Java中类似地完成。

enter image description here


可以预料,创建规则不会引发异常。

1
2
@Rule
public ExpectedException expectedException = ExpectedException.none();


使用断言空(…)

1
2
3
4
5
6
7
8
@Test
public void foo() {
    try {
        //execute code that you expect not to throw Exceptions.
    } catch (Exception e){
        assertNull(e);
    }
}


如果要测试测试目标是否使用异常。只需将测试保留为(使用jmock2模拟合作者):

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void consumesAndLogsExceptions() throws Exception {

    context.checking(new Expectations() {
        {
            oneOf(collaborator).doSth();
            will(throwException(new NullPointerException()));
        }
    });

    target.doSth();
 }

如果目标确实使用抛出的异常,则测试将通过,否则测试将失败。

如果您想测试异常消费逻辑,事情会变得更加复杂。我建议把消费委托给一个可以被嘲笑的合作者。因此,测试可以是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void consumesAndLogsExceptions() throws Exception {
    Exception e = new NullPointerException();
    context.checking(new Expectations() {
        {
            allowing(collaborator).doSth();
            will(throwException(e));

            oneOf(consumer).consume(e);
        }
    });

    target.doSth();
 }

但如果你只是想记录它,有时它是过度设计的。在这种情况下,如果您坚持使用TDD,本文(http://java.dzone.com/articles/monitoring-declarative-transac,http://blog.novoj.net/2008/09/20/testing-aspect-pointcuts-is-there-an-easy-way/)可能会有所帮助。


这可能不是最好的方法,但它可以确保不会从正在测试的代码块中抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.assertj.core.api.Assertions;
import org.junit.Test;

public class AssertionExample {

    @Test
    public void testNoException(){
        assertNoException();
    }    

    private void assertException(){
        Assertions.assertThatThrownBy(this::doNotThrowException).isInstanceOf(Exception.class);
    }

    private void assertNoException(){
        Assertions.assertThatThrownBy(() -> assertException()).isInstanceOf(AssertionError.class);
    }

    private void doNotThrowException(){
        //This method will never throw exception
    }
}

以下各项未通过所有异常的测试,选中或未选中:

1
2
3
4
5
6
7
8
9
@Test
public void testMyCode() {

    try {
        runMyTestCode();
    } catch (Throwable t) {
        throw new Error("fail!");
    }
}