Go 中的单元测试和基准测试的使用

go test 工具

go test 命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以 _test.go 为后缀名的源代码文件都是 go test 测试的一部分,不会被 go build 编译到最终的可执行文件中。

*_test.go 文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数:

类型 格式 作用
测试函数 函数名前缀为Test 测试程序的一些逻辑行为是否正确
基准函数 函数名前缀为Benchmark 测试函数的性能
示例函数 函数名前缀为Example 为文档提供示例文档

go test 命令会遍历所有的 *_test.go 文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

Golang 单元测试对文件名和方法名,参数都有很严格的要求。

  1. 文件名必须以 xx_test.go 命名
  2. 方法必须是 Test 开头
  3. 方法参数必须 t *testing.T
  4. 使用 go test 执行单元测试

go test 的参数

go test 是 go 语言自带的测试工具,其中包含的是两类,单元测试和性能测试

通过 go help test 可以看到 go test 的使用说明:

格式形如:

go test [-c] [-i] [build flags] [packages] [flags for test binary]

参数解读:

  • -c : 编译 go test 成为可执行的二进制文件,但是不运行测试。

  • -i : 安装测试包依赖的 package,但是不运行测试。

  • 关于 build flags,调用 go help build,这些是编译运行过程中需要使用到的参数,一般设置为空

  • 关于 packages,调用 go help packages,这些是关于包的管理,一般设置为空

  • 关于 flags for test binary,调用 go help flags for test binary,这些是 go test 过程中经常使用到的参数

常用示例如下:

  • -test.v : 是否输出全部的单元测试用例(不管成功或者失败),默认没有加上,所以只输出失败的单元测试用例。
  • -test.run pattern: 只跑哪些单元测试用例
  • -test.bench patten: 只跑那些性能测试用例
  • -test.benchmem : 是否在性能测试的时候输出内存情况
  • -test.benchtime t : 性能测试运行的时间,默认是1s
  • -test.cpuprofile cpu.out : 是否输出cpu性能分析文件
  • -test.memprofile mem.out : 是否输出内存性能分析文件
  • -test.blockprofile block.out : 是否输出内部goroutine阻塞的性能分析文件
  • -test.memprofilerate n : 内存性能分析的时候有一个分配了多少的时候才打点记录的问题。这个参数就是设置打点的内存分配间隔,也就是 profile 中一个 sample 代表的内存大小。默认是设置为 512 * 1024 的。如果将它设置为 1,则每分配一个内存块就会在 profile 中有个打点,那么生成的 profile 的 sample 就会非常多。如果设置为0,那就是不做打点了。可以通过设置 memprofilerate=1GOGC=off 来关闭内存回收,并且对每个内存块的分配进行观察。
  • -test.blockprofilerate n: 基本同上,控制的是 goroutine 阻塞时候打点的纳秒数。默认不设置就相当于 -test.blockprofilerate=1,每一纳秒都打点记录一下
  • -test.parallel n : 性能测试的程序并行 cpu 数,默认等于 GOMAXPROCS。
  • -test.timeout t : 如果测试用例运行时间超过t,则抛出 panic
  • -test.cpu 1,2,4 : 程序运行在哪些 CPU 上面,使用二进制的1所在位代表,和 nginx 的 nginx_worker_cpu_affinity 是一个道理
  • -test.short : 将那些运行时间较长的测试用例运行时间缩短

单元测试函数

单元测试函数的格式

测试函数必须导入 testing 包,基本格式如下:

func TestName(t *testing.T) {
    //...    
}

测试函数的名字必须以 Test 开头,可选的后缀名必须是以大写字母开头,例如:

func TestAdd(t *testing.T){ ... }
func TestSum(t *testing.T){ ... }
func TestLog(t *testing.T){ ... }

其中,参数 t 是用于报告测试失败和附加的日志信息的,testing.T 拥有的方法如下:

Cleanup(func())
Error(args ...interface{})
Errorf(format string, args ...interface{})
Fail()
FailNow()
Failed() bool
Fatal(args ...interface{})
Fatalf(format string, args ...interface{})
Helper()
Log(args ...interface{})
Logf(format string, args ...interface{})
Name() string
Skip(args ...interface{})
SkipNow()
Skipf(format string, args ...interface{})
Skipped() bool
TempDir() string

其中,

  • Fail,Error:该测试失败,该测试继续,其他测试继续执行
  • FailNow,Fatal:该测试失败,该测试终止,其他测试继续执行

单元测试函数的使用示例

在 split 包中定义一个 Split 函数,实现如下:

package split

import "strings"

func Split(s, sep string) (result []string) {
    i := strings.Index(s, sep)

    for i > -1 {
        result = append(result, s[:i])
        s = s[i+1:]
        i = strings.Index(s, sep)
    }
    result = append(result, s)
    return
}

在新建一个文件命名为 split_test,内容如下:

package split

import (
    "reflect"
    "testing"
)

func TestSplit(t *testing.T) {
    got := Split("a:b:c", ":")      // 程序输出的结果
    want := []string{"a", "b", "c"} // 期望的结果
    // slice 不能直接比较,借助反射包中的方法进行比价
    if !reflect.DeepEqual(want, got) {
        t.Errorf("excepted:%v, got %v", want, got)
    }
}

在 split 目录下运行 go test 命令:

split » go test
PASS
ok      algorithm/split 0.005s

再写一个多个字符切割字符串的测试用例:

func TestMoreSplit(t *testing.T) {
    got := Split("abcd","bc") // 按照当前 split 的实现,这里 got 应该是[a,cd]
    want := []string{"a","d"}
    if !reflect.DeepEqual(want, got) {
        t.Errorf("excepted:%v, got %v", want, got)
    }
}

再次运行 go test,结果如下:

--- FAIL: TestMoreSplit (0.00s)
    split_test.go:21: excepted:[a d], got [a cd]
FAIL
exit status 1
FAIL    algorithm/split 0.005s

可以看到测试不通过,这里可以通过添加 -v 参数,查看测试函数的名称和运行时间:

// go test -v

=== RUN   TestSplit
--- PASS: TestSplit (0.00s)
=== RUN   TestMoreSplit
    split_test.go:21: excepted:[a d], got [a cd]
--- FAIL: TestMoreSplit (0.00s)
FAIL
exit status 1
FAIL    algorithm/split 0.005s

这样就可以看出哪个函数通过而哪个函数没有通过。

还可以在 go test 命中后添加 -run 参数,它对应一个正则表达式,只有函数名匹配上的测试函数才会被 go test 命令执行。

~ » go test -v -run="More"
=== RUN   TestMoreSplit
    split_test.go:21: excepted:[a d], got [a cd]
--- FAIL: TestMoreSplit (0.00s)
FAIL
exit status 1
FAIL    algorithm/split 0.006s

对于上面代码的 bug 进行修复:

package split

import "strings"

func Split(s, sep string) (result []string) {
    i := strings.Index(s, sep)

    for i > -1 {
        result = append(result, s[:i])
        s = s[i+len(sep):] // 这里使用 len(sep) 获取 sep 的长度
        i = strings.Index(s, sep)
    }
    result = append(result, s)
    return
}

然后重新进行测试,需要注意,修改了代码之后不要仅仅执行那些失败的测试函数,还应该完整的运行所有的测试,保证不会因为修改代码而引入了新的问题

=== RUN   TestSplit
--- PASS: TestSplit (0.00s)
=== RUN   TestMoreSplit
--- PASS: TestMoreSplit (0.00s)
PASS
ok      algorithm/split 0.005s

测试组

接下来在测试一下 split 函数对于中文字符串的支持,这个时候一种方式是再编写一个 TestChineseSplit 测试函数,更好的方式是使用如下的方法来添加更多的测试用例:

package split

import (
    "reflect"
    "testing"
)

func TestSplit(t *testing.T) {
    // 定义一个测试用例类型
    type test struct {
        input string
        sep   string
        want  []string
    }
    // 定义一个存储测试用例的切片
    tests := []test{
        {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
        {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
        {input: "abcd", sep: "bc", want: []string{"a", "d"}},
        {input: "枯藤老树昏鸦", sep: "老", want: []string{"枯藤", "树昏鸦"}},
    }
    // 遍历切片,逐一执行测试用例
    for _, tc := range tests {
        got := Split(tc.input, tc.sep)
        if !reflect.DeepEqual(got, tc.want) {
            // 使用 %#v 格式化的方式
            t.Errorf("excepted:%#v, got:%#v", tc.want, got)
        }
    }
}

自测试 t.Run

对于上面的测试,如果测试用例比较多的时候,是没办法一眼看出来具体是哪个测试用例失败了。可以通过如下的解决方法:

package split

import (
    "fmt"
    "reflect"
    "testing"
)

func TestSplit(t *testing.T) {
    // 定义一个测试用例类型
    type test struct {
        input string
        sep   string
        want  []string
    }
    // 通过 map 存储测试用例
    tests := map[string]test{
        "simple":      {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
        "wrong sep":   {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
        "more sep":    {input: "abcd", sep: "bc", want: []string{"a", "d"}},
        "leading sep": {input: "枯藤老树昏鸦", sep: "老", want: []string{"枯藤", "树昏鸦"}},
    }
    // 遍历切片,逐一执行测试用例
    for name, tc := range tests {
        got := Split(tc.input, tc.sep)
        if !reflect.DeepEqual(got, tc.want) {
            // 使用 %#v 格式化的方式
            t.Errorf("name:%s excepted:%#v, got:%#v", name, tc.want, got)
        }
    }
}

另外,go1.7+ 中新增了子测试,可以按照如下方式使用 t.Run 执行子测试:

package split

import (
    "reflect"
    "testing"
)

func TestSplit(t *testing.T) {
    // 定义一个测试用例类型
    type test struct {
        input string
        sep   string
        want  []string
    }
    // 通过 map 存储测试用例
    tests := map[string]test{
        "simple":      {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
        "wrong sep":   {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
        "more sep":    {input: "abcd", sep: "bc", want: []string{"a", "d"}},
        "leading sep": {input: "枯藤老树昏鸦", sep: "老", want: []string{"枯藤", "树昏鸦"}},
    }
    // 遍历切片,逐一执行测试用例
    for name, tc := range tests {
        t.Run(name, func(t *testing.T) {
            // 使用 t.Run() 执行子测试
            got := Split(tc.input, tc.sep)
            if !reflect.DeepEqual(got, tc.want) {
                // 使用 %#v 格式化的方式
                t.Errorf("name:%s excepted:%#v, got:%#v", name, tc.want, got)
            }
        })
    }
}

运行 go test -v 命令就可以看到清晰的输出内容了:

=== RUN   TestSplit
=== RUN   TestSplit/simple
=== RUN   TestSplit/wrong_sep
=== RUN   TestSplit/more_sep
=== RUN   TestSplit/leading_sep
--- PASS: TestSplit (0.00s)
    --- PASS: TestSplit/simple (0.00s)
    --- PASS: TestSplit/wrong_sep (0.00s)
    --- PASS: TestSplit/more_sep (0.00s)
    --- PASS: TestSplit/leading_sep (0.00s)
PASS
ok      algorithm/split 0.005s

可以通过 -run=RegExp 来指定运行的测试用例,还可以通过 / 来指定要运行的子测试用例,例如:go test -v -run=Split/simple 只会运行 simple 对应的子测试用例

测试覆盖率 go test -cover

测试覆盖率是代码被测试套件覆盖的百分比。通常使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。

GO 提供内置功能来检查代码覆盖率。可以使用 go test -cover 来查看测试覆盖率。例如:

$ go test -cover 

PASS
coverage: 100.0% of statements
ok      algorithm/split 0.005s

可以看到测试用例的代码覆盖率是 100%。

Go 还提供了额外的 -coverprofile 参数,用来将覆盖率相关的记录信息输出到一个文件:

go test -cover -coverprofile=c.out

PASS
coverage: 100.0% of statements
ok      algorithm/split 0.005s

上面的命令会将覆盖率相关的信息输出到当前文件夹下面的 c.out 文件中,然后执行 go tool cover -html=c.out,使用 cover 工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个 HTML 报告:

go cover 报告

基准测试函数

基准测试就是在一定的工作负载下检测程序性能的一种方法。基准测试的基本格式如下:

func BenchmarkName(b *testing.B) {
    // 与性能测试无关的代码
    
    b.ResetTimer()
    for i:=0;i<b.N;i++ {
        // 测试代码
    }
    b.StopTimer()
    
    // 与性能测试无关的代码
}

基准测试以 Benchmark 为前缀,需要一个 *testing.B 类型的参数b,基准测试必须要执行 b.N 次,这样的测试才有对照性,b.N 的值是系统根据实际情况去调整的,从而保证测试的稳定性。 testing.B 拥有的方法如下:

func (c *B) Error(args ...interface{})
func (c *B) Errorf(format string, args ...interface{})
func (c *B) Fail()
func (c *B) FailNow()
func (c *B) Failed() bool
func (c *B) Fatal(args ...interface{})
func (c *B) Fatalf(format string, args ...interface{})
func (c *B) Log(args ...interface{})
func (c *B) Logf(format string, args ...interface{})
func (c *B) Name() string
func (b *B) ReportAllocs()
func (b *B) ResetTimer()
func (b *B) Run(name string, f func(b *B)) bool
func (b *B) RunParallel(body func(*PB))
func (b *B) SetBytes(n int64)
func (b *B) SetParallelism(p int)
func (c *B) Skip(args ...interface{})
func (c *B) SkipNow()
func (c *B) Skipf(format string, args ...interface{})
func (c *B) Skipped() bool
func (b *B) StartTimer()
func (b *B) StopTimer()

基准测试示例

为 split 包中的 Split 函数编写基准测试如下:

func BenchmarkSplit(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Split("枯藤老树昏鸦","老")
    }
}

基准测试并不会默认执行,需要增加 -bench 参数,通过执行 go test -bench=Split 命令执行基准测试,输出结果如下:

$ go test -bench=Split
 
goos: darwin
goarch: amd64
pkg: algorithm/split
cpu: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
BenchmarkSplit-4         9540991               121.5 ns/op
PASS
ok      algorithm/split 1.294s
  • 其中 BenchmarkSplit-4 表示对 Split 函数进行基准测试,数字 4 表示 GOMAXPROCS 的值,这个对于并发基准测试很重要。
  • 9540991 和 121.5 ns/op 表示每次调用 Split 函数耗时 121.5ns,这个结果是 9540991 次调用的平均值。

还可以添加 -benchmem 参数,来获得内存分配的统计数据:

$ go test -bench=Split -benchmem

goos: darwin
goarch: amd64
pkg: algorithm/split
cpu: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
BenchmarkSplit-4         9606978               121.7 ns/op            48 B/op          2 allocs/op
PASS
ok      algorithm/split 1.303s

其中,48 B/op表示每次操作内存分配了 48 字节,2 allocs/op则表示每次操作进行了 2 次内存分配

对 Split 函数进行优化:

func Split(s, sep string) (result []string) {
    // 提前使用 make 为 result 分配一个容量足够大的切片
    result = make([]string,0,strings.Count(s,sep) +1)
    i := strings.Index(s, sep)

    for i > -1 {
        result = append(result, s[:i])
        s = s[i+len(sep):] // 这里使用 len(sep) 获取 sep 的长度
        i = strings.Index(s, sep)
    }
    result = append(result, s)
    return
}

再次运行测试:

$ go test -bench=Split -benchmem

goos: darwin
goarch: amd64
pkg: algorithm/split
cpu: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
BenchmarkSplit-4        10998399                95.64 ns/op           32 B/op          1 allocs/op
PASS
ok      algorithm/split 1.170s

可以看到每次分配的内存将为 32 字节,而且只进行了 1 次内存分配。

性能比较函数

上面的基准测试只能得到给定操作的绝对耗时,但是在很多性能问题是:

  1. 发生在两个不同操作之间的相对耗时,比如同一个函数处理 1000 个元素的耗时与处理 1 万甚至 100 万个元素的耗时的差别是多少?
  2. 再或者对于同一个任务究竟使用哪种算法性能最佳?通常需要对两个不同算法的实现使用相同的输入来进行基准比较测试。

性能比较函数通常是一个带有参数的函数,被多个不同的 Benchmark 函数传入不同的值来调用。举个例子如下:

func benchmark(b *testing.B, size int){/* ... */}
func Benchmark10(b *testing.B){ benchmark(b, 10) }
func Benchmark100(b *testing.B){ benchmark(b, 100) }
func Benchmark1000(b *testing.B){ benchmark(b, 1000) }

例如编写了一个计算斐波那契数列的函数如下:

package fib

func Fib(n int) int {
    if n < 2 {
        return n
    }
    return Fib(n-1) + Fib(n-2)
}

性能测试函数如下:

package fib

import "testing"

func benchmarkFib(b *testing.B,n int) {
    for i := 0; i < b.N; i++ {
        Fib(n)
    }
}

func BenchmarkFib1(b *testing.B) {benchmarkFib(b,1)}
func BenchmarkFib2(b *testing.B) {benchmarkFib(b,2)}
func BenchmarkFib3(b *testing.B) {benchmarkFib(b,3)}
func BenchmarkFib10(b *testing.B) {benchmarkFib(b,10)}
func BenchmarkFib20(b *testing.B) {benchmarkFib(b,20)}
func BenchmarkFib40(b *testing.B) {benchmarkFib(b,40)}

运行基准测试:

$ go test -bench=.
 
goos: darwin
goarch: amd64
pkg: algorithm/fib
cpu: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
BenchmarkFib1-4         654152845                1.772 ns/op
BenchmarkFib2-4         227844997                5.229 ns/op
BenchmarkFib3-4         142425820                8.411 ns/op
BenchmarkFib10-4         3659920               323.4 ns/op
BenchmarkFib20-4           29044             41357 ns/op
BenchmarkFib40-4               2         609840838 ns/op
PASS
ok      algorithm/fib   10.091s

这里需要注意的是,默认情况下,每个基准测试至少运行1秒。如果在 Benchmark 函数返回时没有到 1 秒,则 b.N 的值会按1,2,5,10,20,50,…增加,并且函数再次运行。

最终的 BenchmarkFib40 只运行了两次,每次运行的平均值只有不到一秒。像这种情况下应该可以使用 -benchtime 标志增加最小基准时间,以产生更准确的结果。例如:

$ go test -bench=Fib40 -benchtime=20s
 
goos: darwin
goarch: amd64
pkg: algorithm/fib
cpu: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
BenchmarkFib40-4              38         614229184 ns/op
PASS
ok      algorithm/fib   23.965s

这一次 BenchmarkFib40 运行了 38 次。

使用性能比较函数做测试的时候,一个容易犯的错误就是把 b.N 作为输入的大小,例如:

// 错误示范1
func BenchmarkFibWrong(b *testing.B) {
    for n := 0; n < b.N; n++ {
        Fib(n)
    }
}
 
// 错误示范2
func BenchmarkFibWrong2(b *testing.B) {
    Fib(b.N)
}

重置时间 ResetTimer()

b.ResetTimer() 之前的处理不会被放到执行时间里,也不会输出到报告中,所以可以在之前做一些不计划作为测试报告的操作:

func BenchmarkSplit(b *testing.B) {
    time.Sleep(5 * time.Second) // 假设需要做一些耗时的无关操作
    b.ResetTimer()              // 重置计时器
    for i := 0; i < b.N; i++ {
        Split("枯藤老树昏鸦", "老")
    }
}

并行测试 RunParallel

func (b *B) RunParallel(body func(*PB))

该函数会以并行的方式执行给定的基准测试:

  • RunParallel 会创建出多个 goroutine,并将 b.N 分配给这些 goroutine 执行, 其中 goroutine 数量的默认值为 GOMAXPROCS
  • 用户如果想要增加非CPU受限(non-CPU-bound)基准测试的并行性, 那么可以在 RunParallel 之前调用 SetParallelism 。
  • RunParallel 通常会与 -cpu 标志一同使用。
package split

import "testing"

func BenchmarkSplitParallel(b *testing.B) {
    //b.SetParallelism(1) // 设置使用的 cpu 数量
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            Split("枯藤老树昏鸦", "老")
        }
    })
}

执行测试后:

$ go test -bench=.
 
goos: darwin
goarch: amd64
pkg: algorithm/split
cpu: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
BenchmarkSplitParallel-4        21511131                52.12 ns/op
PASS
ok      algorithm/split 1.185s

结合 pprof 性能监控

package fib

import "testing"

func BenchmarkFib10(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Fib(10)
    }
}

执行命令:

go test -bench=. -benchmem -cpuprofile profile.out
// 还可以同时查看内存
go test -bench=. -benchmem -memprofile memprofile.out -cpuprofile profile.out

这会在当前目录下生成 memprofile.outprofile.out 文件,接下来可以用输出的文件使用 pprof:

go tool pprof profile.out 

Type: cpu
Time: Mar 17, 2021 at 8:22pm (CST)
Duration: 1.65s, Total samples = 1.32s (80.06%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 1320ms, 100% of 1320ms total
      flat  flat%   sum%        cum   cum%
    1300ms 98.48% 98.48%     1320ms   100%  algorithm/fib.Fib
      20ms  1.52%   100%       20ms  1.52%  runtime.newstack
         0     0%   100%     1320ms   100%  algorithm/fib.BenchmarkFib10
         0     0%   100%     1320ms   100%  testing.(*B).launch
         0     0%   100%     1320ms   100%  testing.(*B).runN
(pprof) 

这个是使用 cpu 文件, 也可以使用内存文件

然后也可以用 list 命令检查函数需要的时间:

(pprof) list Fib
Total: 1.32s
ROUTINE ======================== algorithm/fib.BenchmarkFib10 in /Users/silverming/go-project/algorithm/fib/fib_test.go
         0      1.32s (flat, cum)   100% of Total
         .          .      2:
         .          .      3:import "testing"
         .          .      4:
         .          .      5:func BenchmarkFib10(b *testing.B) {
         .          .      6:   for i := 0; i < b.N; i++ {
         .      1.32s      7:           Fib(10)
         .          .      8:   }
         .          .      9:}
ROUTINE ======================== algorithm/fib.Fib in /Users/silverming/go-project/algorithm/fib/fib.go
     1.30s      1.91s (flat, cum) 144.70% of Total
         .          .      1:package fib
         .          .      2:
     260ms      280ms      3:func Fib(n int) int {
     150ms      150ms      4:   if n < 2 {
     170ms      170ms      5:           return n
         .          .      6:   }
     720ms      1.31s      7:   return Fib(n-1) + Fib(n-2)
         .          .      8:}

还可以通过 web 命令生成图像(没有测试成功)。。。

Setup 与 TearDown

测试程序有时需要在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)。

TestMain

通过在 *_test.go 文件中定义 TestMain 函数,可以在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)操作。

如果测试文件包含函数 func TestMain(m *testing.M)那么生成的测试会先调用 TestMain(m),然后再运行具体测试

TestMain 运行在主 goroutine 中, 可以在调用 m.Run 前后做任何设置(setup)和拆卸(teardown)。

退出测试的时候应该使用 m.Run 的返回值作为参数调用 os.Exit

一个使用 TestMain 来设置 Setup 和 TearDown 的示例如下:

func TestMain(m *testing.M) {
    fmt.Println("write setup code here...") // 测试之前的做一些设置
    // 如果 TestMain 使用了 flags,这里应该加上flag.Parse()
    retCode := m.Run()                         // 执行测试
    fmt.Println("write teardown code here...") // 测试之后做一些拆卸工作
    os.Exit(retCode)                           // 退出测试
}

需要注意的是:在调用TestMain时, flag.Parse 并没有被调用。所以如果 TestMain 依赖于 command-line 标志 (包括 testing 包的标记),则应该显示的调用 flag.Parse。

子测试的 Setup 与 Teardown

有时候可能需要为每个测试集设置 Setup 与 Teardown,也有可能需要为每个子测试设置 Setup 与 Teardown。

下面定义两个函数工具函数如下:

// 测试集的 Setup 和 Teardown
func setupTestCase(t *testing.T) func(t *testing.T) {
    t.Log("如有需要哦在此执行:测试之前的 setup")
    return func(t *testing.T) {
        t.Log("如有需要在此执行:测试之后的 teardown")
    }
}

// 子测试的 Setup 和 Teardown
func setupSubTest(t *testing.T) func(t *testing.T) {
    t.Log("如有需要在此执行:子测试之前的 setup")
    return func(t *testing.T) {
        t.Log("如有需要在此执行:子测试之后的 teardown")
    }
}

使用方式如下:

func TestSplit(t *testing.T) {
    type test struct { // 定义test结构体
        input string
        sep   string
        want  []string
    }
    tests := map[string]test{ // 测试用例使用map存储
        "simple":      {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
        "wrong sep":   {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
        "more sep":    {input: "abcd", sep: "bc", want: []string{"a", "d"}},
        "leading sep": {input: "枯藤老树昏鸦", sep: "老", want: []string{"", "枯藤", "树昏鸦"}},
    }
    teardownTestCase := setupTestCase(t) // 测试之前执行 setup 操作
    defer teardownTestCase(t)            // 测试之后执行 teardown 操作

    for name, tc := range tests {
        t.Run(name, func(t *testing.T) { // 使用 t.Run() 执行子测试
            teardownSubTest := setupSubTest(t) // 子测试之前执行 setup 操作
            defer teardownSubTest(t)           // 测试之后执行 teardown 操作
            got := Split(tc.input, tc.sep)
            if !reflect.DeepEqual(got, tc.want) {
                t.Errorf("excepted:%#v, got:%#v", tc.want, got)
            }
        })
    }
}

测试结果如下:

$ go test -v                                                                                                    silverming@ZIMINGXING-MB1
=== RUN   TestSplit
    split_test.go:10: 如有需要哦在此执行:测试之前的 setup
=== RUN   TestSplit/more_sep
    split_test.go:18: 如有需要在此执行:子测试之前的 setup
    split_test.go:20: 如有需要在此执行:子测试之后的 teardown
=== RUN   TestSplit/leading_sep
    split_test.go:18: 如有需要在此执行:子测试之前的 setup
    split_test.go:46: excepted:[]string{"", "枯藤", "树昏鸦"}, got:[]string{"枯藤", "树昏鸦"}
    split_test.go:20: 如有需要在此执行:子测试之后的 teardown
=== RUN   TestSplit/simple
    split_test.go:18: 如有需要在此执行:子测试之前的 setup
    split_test.go:20: 如有需要在此执行:子测试之后的 teardown
=== RUN   TestSplit/wrong_sep
    split_test.go:18: 如有需要在此执行:子测试之前的 setup
    split_test.go:20: 如有需要在此执行:子测试之后的 teardown
=== CONT  TestSplit
    split_test.go:12: 如有需要在此执行:测试之后的 teardown
--- FAIL: TestSplit (0.00s)
    --- PASS: TestSplit/more_sep (0.00s)
    --- FAIL: TestSplit/leading_sep (0.00s)
    --- PASS: TestSplit/simple (0.00s)
    --- PASS: TestSplit/wrong_sep (0.00s)
FAIL
exit status 1
FAIL    algorithm/split 0.005s

示例函数 Example

示例函数的格式

被 go test 特殊对待的第三种函数就是示例函数,它们的函数名以 Example 为前缀。它们既没有参数也没有返回值。标准格式如下:

func ExampleName() {
    //...
}

示例函数示例

为 Split 函数编写一个示例函数如下:

package split_test

import (
    "algorithm/split"
    "fmt"
)

func ExampleSplit() {
    fmt.Println(split.Split("a:b:c",":"))
    fmt.Println(split.Split("枯藤老树昏鸦","老"))
    // Output
    // [a b c]
    // [枯藤 树昏鸦]
}

这个时候函数就会生成相应的文档:

示例函数文档示例

为代码编写示例代码有如下三个用处:

  1. 示例函数能够作为文档直接使用,例如基于 web 的 godoc 中能把示例函数与对应的函数或包相关联。

  2. 示例函数只要包含了 // Output: 也是可以通过 go test 运行的可执行测试。

     split $ go test -run Example
     PASS
     ok      github.com/pprof/studygo/code_demo/test_demo/split       0.006s
    
  3. 示例函数提供了可以直接运行的示例代码,可以直接在 golang.org 的 godoc 文档服务器上使用 Go Playground 运行示例代码。

参考文章:

Go基础:如何做单元测试和基准测试