Go Wiki: 表驱动测试

引言

编写良好的测试并非易事,但在许多情况下,表驱动测试可以覆盖大部分内容:每个表条目都是一个完整的测试用例,包含输入和预期结果,有时还包含其他信息,如测试名称,以便测试输出易于阅读。如果您发现自己在编写测试时使用了复制粘贴,请考虑重构为表驱动测试或将复制的代码提取到辅助函数中是否是更好的选择。

给定一个测试用例表,实际测试只是遍历所有表条目,并为每个条目执行必要的测试。测试代码编写一次,并分摊到所有表条目上,因此编写带有良好错误消息的仔细测试是值得的。

表驱动测试不是一个工具、包或其他什么,它只是一种编写更清晰测试的方式和视角。

表驱动测试示例

这里有一个来自 fmt 包测试代码中的好例子 ( https://pkg.go.dev/fmt/ )

var flagtests = []struct {
    in  string
    out string
}{
    {"%a", "[%a]"},
    {"%-a", "[%-a]"},
    {"%+a", "[%+a]"},
    {"%#a", "[%#a]"},
    {"% a", "[% a]"},
    {"%0a", "[%0a]"},
    {"%1.2a", "[%1.2a]"},
    {"%-1.2a", "[%-1.2a]"},
    {"%+1.2a", "[%+1.2a]"},
    {"%-+1.2a", "[%+-1.2a]"},
    {"%-+1.2abc", "[%+-1.2a]bc"},
    {"%-1.2abc", "[%-1.2a]bc"},
}
func TestFlagParser(t *testing.T) {
    var flagprinter flagPrinter
    for _, tt := range flagtests {
        t.Run(tt.in, func(t *testing.T) {
            s := Sprintf(tt.in, &flagprinter)
            if s != tt.out {
                t.Errorf("got %q, want %q", s, tt.out)
            }
        })
    }
}

请注意 t.Errorf 提供的详细错误消息:它显示了结果和预期结果;输入是子测试的名称。当测试失败时,即使不阅读测试代码,也能立即清楚哪个测试失败了以及为什么。

调用 t.Errorf 并不是断言。即使记录了错误,测试也会继续进行。例如,当测试具有整数输入的某些内容时,了解该函数对所有输入都失败,或者仅对奇数输入失败,或者对二的幂失败是值得的。

使用 Map 存储测试用例

在之前的示例中,测试用例存储在结构体切片中。它们也可以存储在 map 中,并且这样做有几个优点。

tests := map[string]struct {
  input string
  result string
} {
  "empty string":  {
    input: "",
    result: "",
  },
  "one character": {
    input: "x",
    result: "x",
  },
  "one multi byte glyph": {
    input: "🎉",
    result: "🎉",
  },
  "string with multiple multi-byte glyphs": {
    input: "🥳🎉🐶",
    result: "🐶🎉🥳",
  },
}

for name, test := range tests {
  // test := test // NOTE: uncomment for Go < 1.22, see /doc/faq#closures_and_goroutines
  t.Run(name, func(t *testing.T) {
    t.Parallel()
    if got, expected := reverse(test.input), test.result; got != expected {
      t.Fatalf("reverse(%q) returned %q; expected %q", test.input, got, expected)
    }
  })
}

使用 map 的一个优点是,每个测试的“名称”可以简单地作为 map 的索引。

更重要的是,map 的迭代顺序未指定,甚至不保证每次迭代都相同。这确保了每个测试都独立于其他测试,并且测试顺序不会影响结果。

并行测试

并行化表测试很简单,但需要精确操作以避免错误。请仔细注意下面的三个更改,特别是 test 的重新声明。

package main

import (
    "testing"
)

func TestTLog(t *testing.T) {
    t.Parallel() // marks TLog as capable of running in parallel with other tests
    tests := []struct {
        name string
    }{
        {"test 1"},
        {"test 2"},
        {"test 3"},
        {"test 4"},
    }
    for _, test := range tests {
    // test := test // NOTE: uncomment for Go < 1.22, see /doc/faq#closures_and_goroutines
        t.Run(test.name, func(t *testing.T) {
            t.Parallel() // marks each test case as capable of running in parallel with each other 
            t.Log(test.name)
        })
    }
}

参考


此内容是 Go Wiki 的一部分。