操作子测试

TDD 中,测试的范围应该尽量小,且测试结果应该易于理解。我们在第 1 章《掌握测试驱动开发》中探讨了 TDD 的最佳实践。为了实现这些最佳实践,我们需要在测试场景之间进行分离。

让我们考虑到目前为止我们已经编写过的 TestAdd 函数。它当前测试的是两个正数的加法,但我们将扩展它以涵盖负数输入。根据目前的知识,我们有两个选择:

  1. 扩展 TestAdd 的范围来验证新的场景:这种方法会使 Assert 步骤变得更长,但它的优点是可以重用之前的步骤。

  2. 创建一个新的测试来验证新的场景:这种方法会保持 TestAdd 的范围不变,但它的缺点是我们需要重新定义并重新执行 ArrangeAct 步骤。

如果我们选择第二种方法,我们需要为新的测试命名不同的名字。我们将其命名为 TestAdd_Negative,表示我们将在这个测试中测试负数输入。然而,这样的命名不符合现有 TestAdd 函数的命名规则,因此我们需要将现有的测试重命名为 TestAdd_Positive。正如预期的那样,运行测试时会输出不同的行结果:

$ go test -run "^TestAdd" ./chapter02/calculator -v
=== RUN TestAdd_Positive
--- PASS: TestAdd_Positive (0.00s)
=== RUN TestAdd_Negative
--- PASS: TestAdd_Negative (0.00s)
PASS
ok github.com/PacktPublishing/Test-Driven-Developmentin-Go/chapter02/calculator 0.266s

我们希望拥有一个小而独立的测试,但如果我们继续定义新的测试,并且每次测试新的边界情况或场景时可能都需要更改现有测试的名称,这会变得非常繁琐。Go 为我们提供了一个更优雅的解决方案来解决这个常见问题,我们将在下一部分中讨论。

实现子测试

testing.T 类型提供了 Run(name string, f func(t *testing.T)) bool 方法,该方法接受两个参数:

  • 一个 name 参数,类型为字符串

  • 一个函数,该函数接受一个 *testing.T 类型的参数

一旦传递给 Run 方法,测试运行器会将该函数作为当前测试的子测试运行,从而允许我们创建一个测试层次结构,每个层次都有其独立的隔离性。由于外层测试和子测试共享同一个 testing.T 实例,子测试的失败会导致外层测试也失败。这种行为使我们能够根据需求创建多层次的测试结构。以添加正数和负数输入作为测试场景为例,我们可以重构 TestAdd,以充分利用子测试的功能:

func TestAdd(t *testing.T) {
    // Arrange
    e := calculator.Engine{}

    actAssert := func(x, y, want float64) {
        // Act
        got := e.Add(x, y)

        // Assert
        if got != want {
            t.Errorf("Add(%.2f,%.2f) incorrect, got: %.2f, want: %.2f", x, y, got, want)
        }
    }

    t.Run("positive input", func(t *testing.T) {
        x, y := 2.5, 3.5
        want := 6.0
        actAssert(x, y, want)
    })

    t.Run("negative input", func(t *testing.T) {
        x, y := -2.5, -3.5
        want := -6.0
        actAssert(x, y, want)
    })
}

我们创建了一个 actAssert 函数,它接受输入和预期输出作为参数。这个函数将执行 ActAssert 步骤,而无需重复它们。然后,我们使用之前提到的 t.Run 方法创建了两个子测试。每个子测试的名称表示它将涵盖的场景。运行测试将产生以下结果:

$ go test -run "^TestAdd" ./chapter02/calculator -v
=== RUN TestAdd
=== RUN TestAdd/positive_input
=== RUN TestAdd/negative_input
--- PASS: TestAdd (0.00s)
--- PASS: TestAdd/positive_input (0.00s)
--- PASS: TestAdd/negative_input (0.00s)
PASS
ok github.com/PacktPublishing/Test-Driven-Developmentin-Go/chapter02/calculator 0.195s

从输出中我们可以看到,子测试嵌套在外层测试之下。通过利用子测试,我们现在有了一种便捷的方式来创建共享 “准备”(Arrange)步骤的测试,同时也可以轻松扩展更多场景,而无需重命名测试。

我们将在第 4 章《构建高效的测试套件》中讨论与之相关的表驱动测试技术,该技术充分利用了子测试的功能。

代码覆盖率

现在我们知道如何编写涵盖不同场景的测试并运行它们,我们可以查看我们的代码覆盖率。正如我们在第 1 章《掌握测试驱动开发》中提到的,代码覆盖率是一个重要的指标,衡量测试覆盖了多少百分比的代码。

go test 命令有一个 –cover 标志,它计算给定包的代码覆盖率配置文件。它还提供了将配置文件保存到文件的功能,可以通过传递文件路径到 -coverprofile 标志来实现。然后,我们可以查看这些保存的覆盖率配置文件。

让我们对我们的计算器执行这个操作:

$ go test -run "^TestAdd" ./chapter02/calculator -cover -v
=== RUN TestAdd
=== RUN TestAdd/positive_input
=== RUN TestAdd/negative_input
--- PASS: TestAdd (0.00s)
--- PASS: TestAdd/positive_input (0.00s)
--- PASS: TestAdd/negative_input (0.00s)

PASS
coverage: 100.0% of statements
ok github.com/PacktPublishing/Test-Driven-Developmentin-Go/chapter02/calculator 0.113s

此命令在运行所有测试后打印出覆盖率百分比。由于我们的 Add 函数非常简单,因此当前的覆盖率为 100%

现在,让我们使用 go test ./chapter02/calculator -coverprofile=calcCover.out 命令将代码覆盖率配置文件保存到文件中。这将在当前目录中创建 calcCover.out 文件。我们可以使用 Go 工具链中的另一个工具查看这个文件。运行 go tool cover -html=calcCover.out 将在浏览器中打开一个新窗口,展示代码覆盖率的可视化表现。

图 2.8 显示了我们的覆盖率配置文件的可视化表示,表明 Add 方法已被测试覆盖:

image 2025 01 02 11 11 29 607
Figure 1. Figure 2.8 – The visual representation of the saved profile

这涵盖了我们开始编写 Go 测试和进行 TDD 所需了解的所有基本知识。最后,我们需要解决的是如何编写和使用基准测试。