1. 概述

TestNG 是一个流行的Java测试框架,它是JUnit的替代选择。尽管两者都提供了自己的范式,但它们都包含了一个概念:断言,即如果评估为假,则会停止程序执行并失败的逻辑语句。TestNG中的一个简单断言可能如下所示:

@Test 
void testNotNull() {
    assertNotNull("My String"); 
}

但是,如果我们需要在一个测试中执行多个断言呢?在这篇文章中,我们将探讨TestNG的SoftAssert,这是一种同时执行多个断言的技术。

2. 准备工作

为了我们的练习,我们定义一个简单的Book类:

public class Book {
    private String isbn;
    private String title;
    private String author;

    // Standard getters and setters...
}

我们还可以定义一个接口,它模拟了一个基于ISBN查找Book的简单服务:

interface BookService {
    Book getBook(String isbn);
}

然后,我们可以在单元测试中对该服务进行模拟,这将在稍后定义。这个设置让我们能够以一种现实的方式定义一个可以测试的场景:一个返回可能是null的对象或其成员变量可能是null的服务。现在开始编写针对此场景的单元测试。

3. 基本断言与TestNG的SoftAssert

为了展示SoftAssert的好处,我们将首先创建一个使用基本TestNG断言的单元测试,并比较我们得到的反馈与使用SoftAssert的相同测试。

3.1. 使用传统断言

首先,我们将使用assertNotNull()创建一个测试,该方法接受一个要测试的值和可选的消息:

@Test
void givenBook_whenCheckingFields_thenAssertNotNull() {
    Book gatsby = bookService.getBook("9780743273565");

    assertNotNull(gatsby.isbn, "ISBN");
    assertNotNull(gatsby.title, "title");
    assertNotNull(gatsby.author, "author");
}

然后,我们将使用Mockito定义一个模拟的BookService实现,它返回一个Book实例:

@BeforeMethod
void setup() {
    bookService = mock(BookService.class);
    Book book = new Book();
    when(bookService.getBook(any())).thenReturn(book);
}

运行测试时,我们可以看到我们忘记设置了isbn字段:

java.lang.AssertionError: ISBN expected object to not be null

让我们修复这个模拟,再次运行测试:

@BeforeMethod void setup() {
    bookService = mock(BookService.class);
    Book book = new Book();
    book.setIsbn("9780743273565");
    when(bookService.getBook(any())).thenReturn(book);
}

现在我们收到了不同的错误:

java.lang.AssertionError: title expected object to not be null

再次,我们在模拟中忘记了初始化一个字段,导致了另一个必要的更改。

正如我们所见,这种测试、修改和重新运行测试的循环不仅令人沮丧,而且耗时。当然,随着类的大小和复杂性的增加,这种影响会成倍增加。在集成测试的情况下,远程部署环境中的失败可能难以或不可能在本地复现。通常,集成测试更复杂,因此执行时间较长。再加上部署测试变更所需的时间,每次额外的测试重跑的循环时间是昂贵的。

幸运的是,我们可以通过使用SoftAssert来立即不中断程序执行地评估多个断言来避免这个问题

3.2. 使用SoftAssert分组断言

让我们更新上面的示例,使用SoftAssert

@Test void givenBook_whenCheckingFields_thenAssertNotNull() {
    Book gatsby = bookService.getBook("9780743273565"); 
    
    SoftAssert softAssert = new SoftAssert();
    softAssert.assertNotNull(gatsby.isbn, "ISBN");
    softAssert.assertNotNull(gatsby.title, "title");
    softAssert.assertNotNull(gatsby.author, "author");
    softAssert.assertAll();
}

让我们分解一下:

  1. 首先,我们创建一个SoftAssert实例。
  2. 接下来,我们做出关键改变:我们将断言针对SoftAssert实例,而不是使用TestNG的基本assertNonNull()方法
  3. 最后,同样重要的是注意我们需要在准备好获取所有断言结果时调用SoftAssert实例的assertAll()方法

现在,如果我们使用原始模拟(其中没有为Book的任何成员变量值设置值),运行此测试,我们将看到包含所有断言失败的单个错误消息:

java.lang.AssertionError: The following asserts failed:
    ISBN expected object to not be null,
    title expected object to not be null,
    author expected object to not be null

这展示了当一个测试需要多个断言时,使用SoftAssert是一种良好的实践。

3.3. SoftAssert的考虑

虽然SoftAssert设置和使用起来很容易,但有一个重要的注意事项:状态性。由于SoftAssert内部记录每个断言的失败,它不适合跨多个测试方法共享。因此,我们应该确保在每个测试方法中创建一个新的SoftAssert实例

4. 总结

在这篇教程中,我们学习了如何使用TestNG的SoftAssert进行多个断言,并了解了它如何成为编写具有减少调试时间的清洁测试的有用工具。我们还了解到SoftAssert是状态性的,实例不应在多个测试之间共享。

如往常一样,所有的代码都可以在GitHub上找到。