Go Test测试简介
Go语言拥有一套单元测试和性能测试系统,仅需要添加很少的代码就可以快速测试一段需求代码。
go test
命令,会自动读取源码目录下面名为 *_test.go 的文件,生成并运行测试用的可执行文件并输出结果信息。本文中测试用例来源于golang官方文档并且测试结果为golang 1.12.5下完成
测试类型
go test的测试类型分为以下三种
-
单元测试,测试函数需要以
Test
为前缀,例如:func TestXxx(*testing.T)
注意:Xxx 可以是任何字母数字字符串,但是第一个字母不能是小些字母。
在这些函数中,使用 Error, Fail 或相关方法来发出失败信号。
要编写一个新的测试套件,需要创建一个名称以 _test.go 结尾的文件,该文件包含
TestXxx
函数,如上所述。 将该文件放在与被测试的包相同的包中。该文件将被排除在正常的程序包之外,但在运行 “ go test ” 命令时将被包含。 有关详细信息,请运行 “ go help test ” 和 “ go help testflag ” 了解。如果有需要,可以调用
*T
和*B
的 Skip 方法,跳过该测试或基准测试:func TestTimeConsuming(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } // ... }
注意:
testing.Short()
判断是否-test.short flag
是否被设置(后续示例会进行演示) -
基准测试,测试函数需要以
Benchmark
为前缀,例如:func BenchmarkXxx(*testing.B)
通过 "go test" 命令,加上
-bench
flag 来执行,多个基准测试按照顺序运行。基准测试函数如下所示:
func BenchmarkHelloWorld(b *testing.B) { for i := 0; i < b.N; i++ { fmt.Sprintf("Hello World") } }
基准函数会运行目标代码 b.N 次。在基准执行期间,会调整 b.N 直到基准测试函数持续足够长的时间
BenchmarkHelloWorld 1000000 216 ns/op
意味着次基准测试函数在测试时间内循环执行了 1000000 次,每次循环花费 2216 纳秒 (ns)。
注意:为了缩短基准测试,请使用-benchtime命令行标志设置基准测试运行时间,而不要使用
testing.short()
函数跳过部分基准测试。 -
子测试
单元测试和基准测试的Run方法允许定义子测试和子基准测试,而不必为每个子函数定义单独的功能。 这使得诸如
table-driven
测试以及creating hierarchical
成为了可能。 它还提供了一种共享通用设置和拆卸代码的方法:func TestFoo(t *testing.T) { //
t.Run("A=1", func(t *testing.T) { fmt.Println("A=1") }) t.Run("A=2", func(t *testing.T) { fmt.Println("A=2") }) t.Run("B=1", func(t *testing.T) { fmt.Println("B=1") }) // } 每个子测试和子基准测试都有一个唯一的名称:顶级测试的名称和传递给 Run 的名称的组合,以斜杠分隔,并具有用于消歧的可选尾随序列号。
code示例&执行解析
待测试的函数代码
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1) + Fib(n-2)
}
-
单元测试
package codetest import "testing" func TestFib(t *testing.T) { var ( in = 7 expected = 13 ) actual := Fib(in) if actual != expected { t.Errorf("Fib(%d) = %d; expected %d", in, actual, expected) } }
执行
go test .
输出结果$ go test . ok _/Users/xxxxx/Workspace/test/src/codetest 0.005s`
此结果表示测试通过。
我们可以在文件
fib_test.go
新增一个测试函数TestFib1
func TestFib1(t *testing.T) { var ( in = 7 expected = 14 ) actual := Fib(in) if actual != expected { t.Errorf("Fib(%d) = %d; expected %d", in, actual, expected) } }
执行
go test .
输出结果$ go test . --- FAIL: TestFib1 (0.00s) fib_test.go:23: Fib(7) = 13; expected 14 FAIL FAIL _/Users/xxxxx/Workspace/test/src/codetest 0.005s
可以看出由于
TestFib1
测试函数失败进而导致整体的单元测试失败。运行指定单元测试用例
go test
指定文件时默认执行文件内的所有测试用例。可以使用-run
参数选择需要的测试用例单独执行,参考一下的代码。package codetest import "testing" func TestCodeA(t *testing.T) { t.Log("A") } func TestCodeAC(t *testing.T) { t.Log("AC") } func TestCodeB(t *testing.T) { t.Log("B") } func TestCodeC(t *testing.T) { t.Log("C") }
当指定只执行TestCodeA开头的函数时,可以使用如下命令:
go test -v -run TestCodeA
或go test -v -run ^TestCodeA[a-zA-Z0-9]*$
输出结果如下:
=== RUN TestCodeA --- PASS: TestCodeA (0.00s) code_test.go:6: A === RUN TestCodeAC --- PASS: TestCodeAC (0.00s) code_test.go:9: AC PASS ok _/Users/xxxxx/Workspace/test/src/codetest 0.005s
Table-driven覆盖测试
当在测试中需要进行多case的覆盖测试,如果每次都以修改单元测试函数中的方式实现,这样无疑显得很笨拙。在此情况下,我们可以采用 Table-driven 的方式写测试。我们在
lib_test.go
中新增一个测试函数TestFibMore
。func TestFibMore(t *testing.T) { var fibTests = []struct { in int // input expected int // expected result }{ {1, 1}, {2, 1}, {3, 2}, {4, 3}, {5, 5}, {6, 8}, {7, 13}, } for _, test := range fibTests { actual := Fib(test.in) if actual != test.expected { t.Errorf("Fib(%d) = %d; expected %d", tt.in, actual, tt.expected) } } }
$ go test -run TestFibMore PASS ok _/Users/xxxxx/Workspace/test/src/codetest 0.005s
因为我们使用的是
t.Errorf
,其中某个 case 失败,并不会终止测试执行。如下所示:将
TestFibMore
函数case中的{7,13}
改为{7,14}
,并再次运行测试$ go test -run TestFibMore --- FAIL: TestFibMore (0.00s) fib_test.go:44: Fib(7) = 13; expected 14 FAIL exit status 1 FAIL _/Users/xxxxx/Workspace/test/src/codetest 0.004s
-
基准测试
func BenchmarkFib(b *testing.B) { for n := 0; n < b.N; n++ { Fib(10) } }
运行指定基准测试用例
若单元测试函数与基准测试函数在同一包下时或同一个文件时,可以通过在命令中添加
-run=none
或者-run=^$
来规避掉单元测试的运行运行测试命令
go test -run=none -bench .
后输出:go test -run=none -bench . goos: darwin goarch: amd64 BenchmarkFib-12 5000000 295 ns/op PASS ok _/Users/xxxxx/Workspace/test/src/codetest 1.791s
指定运基准测试运行时间
我们还可以通过在命令中加入
-benchtime=60m
来设置基准测试的运行时间。$ go test -run=none -benchtime=10s -bench . goos: darwin goarch: amd64 BenchmarkFib-12 50000000 306 ns/op PASS ok _/Users/xxxxx/Workspace/test/src/codetest 15.618s
嵌套基准测试函数
同时我们还可以通过创建一个基础函数并在此基础上衍生出更多基准测试的方法来进行多case情况的测试
func BenchmarkFib1(b *testing.B) { benchmarkFib(1, b) } func BenchmarkFib2(b *testing.B) { benchmarkFib(2, b) } func BenchmarkFib3(b *testing.B) { benchmarkFib(3, b) } func BenchmarkFib10(b *testing.B) { benchmarkFib(10, b) } func BenchmarkFib20(b *testing.B) { benchmarkFib(20, b) } func BenchmarkFib40(b *testing.B) { benchmarkFib(40, b) } func benchmarkFib(i int, b *testing.B) { for n := 0; n < b.N; n++ { Fib(i) } }
运行测试命令
go test -run=none -bench .
后输出:$ go test -run=none -bench . goos: darwin goarch: amd64 BenchmarkFib1-12 2000000000 1.54 ns/op BenchmarkFib2-12 300000000 4.97 ns/op BenchmarkFib3-12 200000000 8.07 ns/op BenchmarkFib10-12 5000000 298 ns/op BenchmarkFib20-12 50000 38077 ns/op BenchmarkFib40-12 2 589032380 ns/op PASS ok _/Users/xxxxx/Workspace/test/src/codetest 13.554s
注意:输出中的
-12
应为系统的最大核心数内存统计
在运行相关基准测试时,可以使用
-benchmem
参数查看基准测试的内存使用情况。$ go test -run=none -benchmem -bench . goos: darwin goarch: amd64 BenchmarkFib1-12 2000000000 1.55 ns/op 0 B/op 0 allocs/op BenchmarkFib2-12 300000000 4.86 ns/op 0 B/op 0 allocs/op BenchmarkFib3-12 200000000 7.96 ns/op 0 B/op 0 allocs/op BenchmarkFib10-12 5000000 295 ns/op 0 B/op 0 allocs/op BenchmarkFib20-12 50000 36733 ns/op 0 B/op 0 allocs/op BenchmarkFib40-12 2 555686536 ns/op 0 B/op 0 allocs/op PASS ok _/Users/xxxxx/Workspace/test/src/codetest 13.327s
从运行结果中看出在测试结果中增加了两列数据分别为
xx B/op
与xx allocs/op
,分别代表在-一次循环中分配的内存大小和一次循环中进行了多少次内存的分配。计时器的使用
-
StartTimer()
开始计时测试。在基准测试开始之前会自动调用此函数,但也可以用于在调用StopTimer之后恢复计时。
-
StopTimer()
停止计时测试。 这可用于在执行您不想测量的复杂初始化时暂停计时器。
-
ResetTimer()
将经过的基准时间和内存分配计数器归零,并删除用户报告的指标。 它不影响计时器是否正在运行。
func BenchmarkBigLen(b *testing.B) { // If a benchmark needs some expensive setup // before running, the timer may be reset big := NewBig() b.ResetTimer() for i := 0; i < b.N; i++ { big.Len() } }
上面的示例来源于go官方testing package中,我也没有在golang本身的代码库中找到
NewBig
函数的定义,推测只是代表耗时操作吧。并行执行基准测试
如果基准测试需要在并行设置中测试性能,则可以使用 RunParallel 辅助函数 ; 这样的基准测试一般与
go test -cpu
标志一起使用。RunParallel 会创建出多个 goroutine,并将 b.N 分配给这些 goroutine 执行,其中 goroutine 数量的默认值为 GOMAXPROCS。如果想要增加goroutine的数量,可以调用函数SetParallelism将RunParallel使用的goroutine的数量设置为p * GOMAXPROCS。RunParallel
函数将在每个 goroutine 中执行,这个函数需要设置所有 goroutine 本地的状态,并迭代直到pb.Next
返回 false 值为止。func BenchmarkTemplateParallel(b *testing.B) { // b.SetParallelism(2) templ := template.Must(template.New("test").Parse("Hello, {{.}}!")) b.RunParallel(func(pb *testing.PB) { // 每个 goroutine 有属于自己的 bytes.Buffer. var buf bytes.Buffer for pb.Next() { // 所有 goroutine 一起,循环一共执行 b.N 次 buf.Reset() templ.Execute(&buf, "World") } }) }
运行结果
$ go test -run=none -cpu=6 -bench . goos: darwin goarch: amd64 BenchmarkTemplateParallel-6 30000000 48.9 ns/op PASS ok _/Users/xxxxx/Workspace/test/src/codetest 1.531s
基准测试结果
BenchmarkResult
结构包含了基准测试运行后的结果。type BenchmarkResult struct { N int // The number of iterations. T time.duration // The total time taken. Bytes int64 // Bytes processed in one iteration. MemAllocs uint64 // The total number of memory allocations; add in Go 1.1 MemBytes uint64 // The total number of bytes allocated; add in Go 1.1 // Extra records aditional metrics reported by ReportMetric. Extra map[string]float64 // Go 1.13 }
package main import ( "bytes" "fmt" "testing" "text/template" ) func main() { benchmarkResult := testing.Benchmark(func(b *testing.B) { templ := template.Must(template.New("test").Parse("Hello, {{.}}!")) // RunParallel will create GOMAXPROCS goroutines // and distribute work among them. b.RunParallel(func(pb *testing.PB) { // Each goroutine has its own bytes.Buffer. var buf bytes.Buffer for pb.Next() { // The loop body is executed b.N times total across all goroutines. buf.Reset() templ.Execute(&buf, "World") } }) }) fmt.Printf("%s\t%s\n", benchmarkResult.String(), benchmarkResult.MemString()) }
运行结果如下所示:
$ go run main.go 30000000 46.0 ns/op 48 B/op 1 allocs/op
具体函数定义及解释可以参考golang BenchmarkResult
-
-
子测试
package codetest import ( "fmt" "testing" ) func TestFoo(t *testing.T) { //
t.Run("A=1", func(t *testing.T) { fmt.Println("A=1") }) t.Run("A=2", func(t *testing.T) { fmt.Println("A=2") }) t.Run("B=1", func(t *testing.T) { fmt.Println("B=1") }) // } func TestUoo(t *testing.T) { // t.Run("U=1", func(t *testing.T) { fmt.Println("U=1") }) t.Run("A=1", func(t *testing.T) { fmt.Println("A=1") }) // } -run
和-bench
命令行标志的参数是与测试名称相匹配的非固定的正则表达式。对于具有多个斜杠分隔元素(例如子测试)的测试,该参数本身是斜杠分隔的,其中表达式依次匹配每个名称元素。因为它是非固定的,一个空的表达式匹配任何字符串。例如,使用 " 匹配 " 表示 " 其名称包含 ":$ go test -run '' # Run 所有测试。 A=1 A=2 B=1 U=1 A=1 PASS ok _/Users/xxxxx/Workspace/test/src/codetest 0.004s
$ go test -run Foo # Run 匹配 "Foo" 的顶层测试,例如 "TestFooBar"。 A=1 A=2 B=1 PASS ok _/Users/xxxxx/Workspace/test/src/codetest 0.005s
$ go test -run Foo/A= # 匹配顶层测试 "Foo",运行其匹配 "A=" 的子测试。 A=1 A=2 PASS ok _/Users/xxxxx/Workspace/test/src/codetest 0.004s
$ go test -run /A=1 # 运行所有匹配 "A=1" 的子测试。 A=1 A=1 PASS ok _/Users/xxxxx/Workspace/test/src/codetest 0.004s
子测试也可用于控制并行性。所有的子测试完成后,父测试才会完成。在这个例子中,所有的测试是相互并行运行的,当然也只是彼此之间,不包括定义在其他顶层测试的子测试:
func TestGroupedParallel(t *testing.T) { names := []string{"aa", "bb", "cc"} for _, name := range names { tName := name t.Run(tName, func(t *testing.T) { t.Parallel() fmt.Println(tName) }) } }
执行命令
go test -run .
后输出如下:$ go test -run . aa cc bb PASS ok _/Users/xxxxx/Workspace/test/src/codetest 0.004s
在并行子测试完成之前,Run 方法不会返回,这提供了一种测试后清理的方法:
func TestTeardownParallel(t *testing.T) { // This Run will not return until the parallel tests finish. t.Run("group", func(t *testing.T) { t.Run("Test1", parallelTest1) t.Run("Test2", parallelTest2) t.Run("Test3", parallelTest3) }) //
} func parallelTest1(t *testing.T) { fmt.Println("Test1") } func parallelTest2(t *testing.T) { fmt.Println("Test2") } func parallelTest3(t *testing.T) { fmt.Println("Test3") }
常见问题解析
-
超时报错
*** Test killed with quit: ran too long (10m0s).
出现此种类型的错误是因为golang test存在默认的超时间为10m。
-timeout d If a test binary runs longer than duration d, panic. If d is 0, the timeout is disabled. The default is 10 minutes (10m).
若想要更长时间的运行测试,可以在命令中加入
-timeout=60m
参数,参数接受s、m、h等级别的时间参数