如何测试没有抛出异常?

IT小君   2021-10-06T16:28:25

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

@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?)

评论(17)
IT小君

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

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

单元测试的背景

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

或者,如Roy OsheroveThe art of Unit Testing, 2nd Edition,第 11 页中所定义:

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

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

在此处输入图片说明

理想情况下,您应该为每个单独的工作单元制定一个测试方法,以便您始终可以立即查看哪里出错了。在这个例子中,调用了一个基本方法getUserById(),它将返回一个用户,总共有 3 个工作单元。

第一个工作单元应该测试在有效和无效输入的情况下是否返回有效用户。
数据源抛出的任何异常都必须在此处处理:如果没有用户存在,则应该进行测试以证明在找不到用户时抛出异常。这方面的一个示例可能IllegalArgumentException是被@Test(expected = IllegalArgumentException.class)注释捕获的

一旦你处理了这个基本工作单元的所有用例,你就上升了一个层次。在这里你做的完全一样,但你只处理来自当前级别正下方级别的异常。这使您的测试代码结构良好,并允许您快速运行架构以找出问题所在,而不必到处跳。

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

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

当您使用有效输入时,您正在设置隐式期望,无论您编写什么测试,都将起作用。

这种方法调用可以是这样的:existingUserById_ShouldReturn_UserObject如果此方法失败(例如:抛出异常),则您知道出了点问题,您可以开始挖掘。

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

TL; 博士

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

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

2021-10-06T16:28:25   回复
IT小君

我偶然发现了这一点,因为 SonarQube 的规则“squid:S2699”:“向这个测试用例添加至少一个断言。”

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

考虑这个简单的代码:

public class Printer {

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

可以添加什么样的断言来测试这个方法?当然,您可以尝试捕获它,但这只是代码膨胀。

解决方案来自 JUnit 本身。

如果没有抛出异常并且您想明确说明此行为,只需添加expected如下示例:

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

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

2021-10-06T16:28:25   回复
IT小君

JUnit 5 (Jupiter) 提供了三个函数来检查异常是否存在/存在:

assertAll​()

断言所有供应executables
  并不会抛出异常。

assertDoesNotThrow​()

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

  此功能
  自JUnit 5.2.0(2018 年 4 月 29 日)可用

assertThrows​()

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

例子

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));
    }

}
2021-10-06T16:28:26   回复
IT小君

使用AssertJ 流畅的断言 3.7.0

Assertions.assertThatCode(() -> toTest.method())
    .doesNotThrowAnyException();
2021-10-06T16:28:26   回复
IT小君

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()
}

尽管有很多空间可以摆弄您想要如何表达这一点,但我一直是流畅断言的粉丝


关于

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

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

Java 允许控制流的异常。这是由 JRE 运行时本身在 API 中完成的,例如Double.parseDoublevia aNumberFormatExceptionPaths.getvia a InvalidPathException

鉴于您已经编写了一个验证数字字符串的组件Double.ParseDouble,可能使用正则表达式,可能是手写解析器,或者可能嵌入了一些其他域规则,将 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使用Theories或,Parameterized以便您可以更轻松地将此测试重新用于其他输入。或者,如果您想要异国情调,您可以使用测试生成工具(和这个)。TestNG 对参数化测试有更好的支持。

我觉得特别不愉快的是使用的建议@Test(expectedException=IllegalArgumentException.class)这个例外非常广泛如果您的代码发生更改,使得被测组件的构造函数具有if(constructorArgument <= 0) throw IllegalArgumentException(),并且您的测试为该参数提供 0,因为它很方便——这很常见,因为生成测试数据是一个非常困难的问题——,那么您的测试即使它什么都不测试,也会是绿条。这样的测试比无用还要糟糕。

2021-10-06T16:28:26   回复
IT小君

如果您不幸捕获了代码中的所有错误。你可以愚蠢地做

class DumpTest {
    Exception ex;
    @Test
    public void testWhatEver() {
        try {
            thisShouldThrowError();
        } catch (Exception e) {
            ex = e;
        }
        assertEquals(null,ex);
    }
}
2021-10-06T16:28:26   回复
IT小君

虽然这篇文章已经 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 的人

2021-10-06T16:28:26   回复
IT小君

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

assertAll( () -> foo() )

来源:JUnit 5 API

2021-10-06T16:28:27   回复
IT小君

使用 void 方法测试场景,例如

void testMeWell() throws SomeException {..}

不抛出一个异常:

Junit5

assertDoesNotThrow(() -> {
    testMeWell();
});
2021-10-06T16:28:27   回复
IT小君

使用assertNull(...)

@Test
public void foo() {
    try {
        //execute code that you expect not to throw Exceptions.
    } catch (Exception e){
        assertNull(e);
    }
}
2021-10-06T16:28:27   回复
IT小君

我遇到了同样的情况,我需要检查异常是否在应该的时候抛出,并且只在应该的时候抛出。最终使用以下代码使用异常处理程序对我有利:

    try {
        functionThatMightThrowException()
    }catch (Exception e){
        Assert.fail("should not throw exception");
    }
    RestOfAssertions();

对我来说的主要好处是它非常简单,并且在相同的结构中检查“当且仅当”的另一种方式真的很容易

2021-10-06T16:28:27   回复
IT小君

如果您想测试您的测试目标是否消耗异常。只需将测试保留为(使用 jMock2 的模拟合作者):

@Test
public void consumesAndLogsExceptions() throws Exception {

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

    target.doSth();
 }

如果您的目标确实消耗了抛出的异常,则测试将通过,否则测试将失败。

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

@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();
 }

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

2021-10-06T16:28:27   回复
IT小君

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

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
    }
}
2021-10-06T16:28:27   回复
IT小君

您可以期望通过创建规则不会抛出异常。

@Rule
public ExpectedException expectedException = ExpectedException.none();
2021-10-06T16:28:27   回复
IT小君

您可以通过使用@Rule 然后调用方法 reportMissingExceptionWithMessage 来做到这一点,如下所示: 这是 Scala 代码。

在此处输入图片说明

2021-10-06T16:28:28   回复
IT小君

您可以根据 junit 的断言创建任何类型的自己的断言:

static void assertDoesNotThrow(Executable executable) {
    assertDoesNotThrow(executable, "must not throw");
}
static void assertDoesNotThrow(Executable executable, String message) {
    try {
        executable.execute();
    } catch (Throwable err) {
        fail(message);
    }
}

并测试:

//the following will succeed
assertDoesNotThrow(()->methodMustNotThrow(1));
assertDoesNotThrow(()->methodMustNotThrow(1), "fail with specific message: facepalm");
//the following will fail
assertDoesNotThrow(()->methodMustNotThrow(2));
assertDoesNotThrow(()-> {throw new Exception("Hello world");}, "Fail: must not trow");

一般来说,在任何情况下,在任何有意义的地方,都有可能立即失败(“bla bla bla”)测试。例如,如果在测试用例中抛出任何内容,则在 try/catch 块中使用它会失败:

try{methodMustNotThrow(1);}catch(Throwable e){fail("must not throw");}
//or
try{methodMustNotThrow(1);}catch(Throwable e){Assertions.fail("must not throw");}

这是我们测试的方法的示例,假设我们有这样一个方法,在特定情况下一定不会失败,但它可能会失败:

void methodMustNotThrow(int x) throws Exception{
    if (x == 1) return;
    throw new Exception();
}

上面的方法是一个简单的例子。但这适用于复杂的情况,在这种情况下,失败并不那么明显。有进口:

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import static org.junit.jupiter.api.Assertions.*;
2021-10-06T16:28:28   回复
IT小君

以下所有异常的测试失败,检查或未检查:

@Test
public void testMyCode() {

    try {
        runMyTestCode();
    } catch (Throwable t) {
        throw new Error("fail!");
    }
}
2021-10-06T16:28:28   回复