与断言框架一起工作

虽然 testify/mock 的功能在创建 mocks 时非常有用,但 testify 最著名的还是其断言框架。在本节中,我们将探讨一些常见的断言框架,以及如何使用它们进一步简化和扩展我们的测试。

到目前为止,我们一直通过 if 语句和在 testing.T 参数上调用正确的失败方法来编写验证:

// Assert
if err != nil {
    t.Fatal(err)
}

这种方法简单,但也有以下几个缺点:

  • 重复性:如果测试较长或较复杂,可能需要进行多次断言。我们就必须重复这个错误断言块多次,使得测试变得冗长。

  • 难以进行高级断言:我们希望在验证 mocks 时,能对整个测试过程中的验证有同样的细粒度控制。

  • 与其他语言完全不同:这种方法与其他编程语言完全不同,后者通常拥有强大的 mock 和断言框架。例如,JavaJUnit 就是一个这样的例子。

虽然 Go 标准库没有提供断言功能,但有两个流行的断言框架提供了这种功能:

  • testify 是一个开源的断言框架,提供了一个易于使用且功能强大的断言包。assert 包提供了这类功能。你可以在 testify GitHub 页面 中阅读更多内容。

  • ginkgo 是一个开源的断言框架,提供了行为驱动开发(BDD)风格的测试编写和断言功能。你可以在 ginkgo GitHub 页面 中阅读更多内容。采用这种风格的测试允许开发人员编写类似自然语言的测试。

我们将在第 5 章《执行集成测试》中讨论 BDD 风格的测试。因此,我们将把讨论此类测试的内容留到那时。我们将继续使用 testify 框架来进行当前的探索。

使用 testify

assert 包提供了许多有用的函数,用于创建细粒度的断言。以下是一些你会经常遇到的常见断言:

  • 相等断言assert.Equal 函数允许你检查两个对象是否相等。如果被检查的类型是指针类型,将会对引用值进行值的检查。相对的断言函数 assert.NotEqual 也存在:

    assert.Equal(t, expected, actual)
    assert.NotEqual(t, expected, actual)
  • 空值断言assert.Equal 函数不适用于 nil 值。相反,应该使用 assert.Nil 方法。相对的断言函数 assert.NotNil 也存在:

    assert.Nil(t, actual)
    assert.NotNil(t, actual)
  • 包含断言assert.Contains 函数验证指定的值是否包含在一个字符串、列表或映射中。相对的断言函数 assert.NotContains 也存在:

    assert.Contains(t, collection, element)
    assert.NotContains(t, collection, element)
  • 子集断言assert.Subset 函数验证指定的子集中的所有值是否包含在指定的列表中。相对的断言函数 assert.NotSubset 也存在:

    assert.Subset(t, list, subset)
    assert.NotSubset(t, list, subset)

testify/require 包也提供相同的断言,但在断言失败的情况下会终止测试。如果出现致命的测试错误时,应该使用这个包。例如,我们可以用以下一行代码替换之前的 if 语句,它会在断言失败时调用 t.Fatal

require.Nil(t, err)
扩展 testing

你应该使用断言框架来补充 testing 包的简洁性。当你开始编写更多 Go 代码时,你应该熟悉这些断言框架的功能,并开始在测试中使用它们。

断言错误

在讨论断言时,最后一个方面是如何验证错误。有时,我们不仅需要验证错误是否发生,还需要验证返回的错误消息是否正确。你应该确保在适当的时候验证错误消息。

assert.EqualError 函数验证返回的错误是否为非 nil,并且其消息是否与提供的字符串相等。这使得验证错误消息变得非常容易。与我们之前看到的所有函数一样,require 包也提供了这个函数。

让我们看一个验证错误场景的示例:

t.Run("invalid operation", func(t *testing.T) {
    // Arrange
    expr := "2 % 3"
    operator := "%"
    operands := []float64{2.0, 3.0}
    expectedErrMsg := "bad operator"
    engine := mocks.NewOperationProcessor(t)
    validator := mocks.NewValidationHelper(t)
    parser := input.NewParser(engine, validator)
    validator.On("CheckInput", operator, operands).
        Return(fmt.Errorf(expectedErrMsg)).Once()

    // Act
    result, err := parser.ProcessExpression(expr)

    // Assert
    require.NotNil(t, err)
    require.Nil(t, result)
    assert.Contains(t, err.Error(), expr)
    assert.Contains(t, err.Error(), expectedErrMsg)
    validator.AssertExpectations(t)
})

该测试创建了一个变量 expectedErrMsg,它代表了 mock 将返回的错误消息。然后,这个消息被传递给 assert.Contains 函数,用于验证它是否出现在 ProcessExpression 方法返回的错误中。

自定义错误类型

你也可以创建自己的自定义错误类型,而不仅仅依赖 Go 内建的 error 类型。这样可以在错误检查时提供类型安全,而不是仅仅依赖可能会改变的错误消息,这样可以避免测试变得脆弱。

Mock 和断言框架是我们用来轻松编写测试的工具。然而,即使是最熟练的测试编写者,也会在测试设计不良的代码时遇到困难。通过 TDD 的迭代过程以及良好的软件设计原则,将会得到可测试、可维护的代码。