Go testing

121 阅读2分钟

Go 测试

Go 测试由 go test 命令和 testing 包构成。

  • 测试函数必须写成类似于func TestXxx(*testing.T)的格式
  • 测试文件可以和被测试的在同一个package中,或者在被测试包名_test包中

如果测试文件和被测试文件在同一个包中,那么该测试函数是未被导出的

package abs

import "testing"

func TestAbs(t *testing.T) {
    got := Abs(-1)
    if got != 1 {
        t.Errorf("Abs(-1) = %d; want 1", got)
    }
}

如果使用独立的_test包,那么要测试的包必须显式导入,并且只能测试被导出的函数(黑盒测试)

package abs_test

import (
	"testing"

	"path_to_pkg/abs"
)

func TestAbs(t *testing.T) {
    got := abs.Abs(-1)
    if got != 1 {
        t.Errorf("Abs(-1) = %d; want 1", got)
    }
}

Go 基准测试

函数必须形如func BenchmarkXxx(*testing.B),使用go test -bench来运行基准测试

func BenchmarkRandInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        rand.Int()
    }
}
// 这个基准测试函数会运行 b.N 次,b.N的大小会自动进行调整,让测试的时间保持在一个合理的范围内
// 假如测试结果为 “BenchmarkRandInt-8   	68453040	        17.8 ns/op”
// 说明了被运行了68453040次,每次消耗17.8ns

如果需要在基准测试之前做一些初始化的工作,可以使用ResetTimer来重置计时器

func BenchmarkRandInt(b *testing.B) {
    ... // expensive setup
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        rand.Int()
    }
}

Go 示例测试

Go 语言还能运行并验证代码,实例函数必须形如func ExampleXxx(),并且在后面加上Output注释,Output注释中的内容会被用于验证输出结果(Output 注释内容前后的空格会被忽略)

func ExampleSalutations() {
    fmt.Println("hello, and")
    fmt.Println("goodbye")
    // Output:
    // hello, and
    // goodbye
}

Output注释可以添加前缀来说明输出是无序的

func ExamplePerm() {
    for _, value := range Perm(5) {
        fmt.Println(value)
    }
    // Unordered output: 4
    // 2
    // 1
    // 3
    // 0
}

示例函数如果没有Output注释,那么它只会被编译,不会被执行 以下是命名规范:

func Example() { ... }    // 包的示例
func ExampleF() { ... }   // 函数F的示例
func ExampleT() { ... }   // 类型T的示例
func ExampleT_M() { ... } // 类型T的方法M的示例

可以使用同一个后缀来对示例函数进行分组,后缀必须小写开头

// suffix可以是package/type/function/method名
func Example_suffix() { ... }
func ExampleF_suffix() { ... }
func ExampleT_suffix() { ... }
func ExampleT_M_suffix() { ... }

Go 模糊测试

go test -fuzz支持模糊测试,使用随机生成的数据来测试程序的正确性,模糊测试函数必须形如func FuzzXxx(t *testing.F, args ...T)
fuzzing 的参数只支持以下几种类型

string, []byte
int, int8, int16, int32/rune, int64
uint, uint8/byte, uint16, uint32, uint64
float32, float64
bool

基本格式如下图:

func FuzzHex(f *testing.F) {
  for _, seed := range [][]byte{{}, {0}, {9}, {0xa}, {0xf}, {1, 2, 3, 4}} {
    f.Add(seed)
  }
  f.Fuzz(func(t *testing.T, in []byte) { // in 是根据输入的种子随机生成的
    enc := hex.EncodeToString(in)
    out, err := hex.DecodeString(enc)
    if err != nil {
      t.Fatalf("%v: decode: %v", in, err)
    }
    if !bytes.Equal(in, out) {
      t.Fatalf("%v: not equal after round trip: %v", in, out)
    }
  })
}
  • 每一个 fuzz test 维护了一个语料库,可以用f.Add向这个语料库中添加模糊测试的种子,或者将种子放在testdata/fuzz/<Name>文件里(Name 是某次模糊测试的名字)。
  • fuzzing 基于种子语料库生成随机输入,种子是可选的,但是提供一些代码覆盖比较好的测试样例给 fuzzing engine 时,engine 将会更有效的发现 bug。
  • 如果 fuzz 测试的时候失败了,那么 fuzzing engine 会将失败的输入保存在testdata/fuzz/<Name>文件里,这个失败的输入将会被当作下一次 fuzzing 的种子。

跳过某一测试

测试或者基准测试可以通过调用Skip方法来跳过,示例如下:

func TestTimeConsuming(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping test in short mode.")
    }
    ...
}

子测试和子基准测试

测试和基准测试允许定义子测试和子基准测试,而不必为每个测试定义单独的函数。这使得像表驱动的基准测试和创建分层测试这样的用法成为可能。它还提供了一种共享公共设置和拆卸代码的方法:

func TestFoo(t *testing.T) {
    // <setup code>
    t.Run("A=1", func(t *testing.T) { ... })
    t.Run("A=2", func(t *testing.T) { ... })
    t.Run("B=1", func(t *testing.T) { ... })
    // <tear-down code>
}

子测试同样可以用于并发控制,只有当所有子测试都完成后,夫测试才会完成。

// 在这个例子中,每个子测试都会并发执行
func TestGroupedParallel(t *testing.T) {
    for _, tc := range tests {
        tc := tc // capture range variable
        t.Run(tc.Name, func(t *testing.T) {
            t.Parallel()
            ...
        })
    }
}

go test -run 使用方法

go test -run ''        # Run all tests.
go test -run Foo       # Run top-level tests matching "Foo", such as "TestFooBar".
go test -run Foo/A=    # For top-level tests matching "Foo", run subtests matching "A=".
go test -run /A=1      # For all top-level tests, run subtests matching "A=1".
go test -fuzz FuzzFoo  # Fuzz the target matching "FuzzFoo"

Main

  • 对于每个测试包,都可以定义一个名为TestMain的函数,它具有如下签名:func TestMain(m *testing.M)
  • 所有测试都会首先调用TestMain而不是直接运行测试或者基准测试,并且可以在调用m.Run前后进行任何 setup 和 teardown 操作。
  • m.Run会运行测试包中的测试并返回一个返回值,该返回值可以传递给os.Exit

最简单的一个例子:

func TestMain(m *testing.M) {
	// call flag.Parse() here if TestMain uses flags
	os.Exit(m.Run())
}