1. 引言
上一篇文章中,介绍了如何通过 go test 实现单元测试: 测试驱动开发与 golang 单元测试
但单元测试只是 go test 最为基础的用法,本文就来介绍 go test 更为进阶的基准测试和并发安全测试。
2. 基准测试 — benchmark test
很多时候,我们不仅需要测试程序执行的正确性,对于程序执行的性能消耗我们往往更加看重,毕竟在项目上线前,究竟需要多少资源来部署项目,项目能够承受多大的流量,不对这些了然于胸,就无法保证线上业务的安全,后果将会是灾难性的。 go test 工具同样也提供了压力测试等功能的支持 — benchmark test。
2.1. 基准测试的编写与执行
go test 的基准测试提供了将目标代码段执行 N 次统计运行时间,从而实现压测的功能。 与单元测试类似,只要在项目的 xxx_test.go 文件中写入下面的方法即可实现基准测试函数的编写:
代码语言:javascript复制func BenchmarkXxx(*testing.B) {
// 测试函数体
}
例如:
代码语言:javascript复制func BenchmarkFib10(b *testing.B) {
for n := 0; n < b.N; n {
Fib(10)
}
}
执行:
go test -bench=.
就会展示:
$ go test -bench=. BenchmarkFib10-4 3000000 424 ns/op PASS ok chapter09/testing 1.724s
表示执行了 3000000 次,耗时 1.724 秒,每次调用 424 纳秒。
2.2. 设定执行时间
我们也可以通过 -benchtime 来指定测试的执行时间:
$ go test -bench=Fib40 -benchtime=20s BenchmarkFib40-4 30 838675800 ns/op
3. testing.B
代码语言:javascript复制type B struct {
common
importPath string // import path of the package containing the benchmark
context *benchContext
N int
previousN int // number of iterations in the previous run
previousDuration time.Duration // total duration of the previous run
benchFunc func(b *B)
benchTime benchTimeFlag
bytes int64
missingBytes bool // one of the subbenchmarks does not have bytes set.
timerOn bool
showAllocResult bool
result BenchmarkResult
parallelism int // RunParallel creates parallelism*GOMAXPROCS goroutines
// The initial states of memStats.Mallocs and memStats.TotalAlloc.
startAllocs uint64
startBytes uint64
// The net total of this test after being run.
netAllocs uint64
netBytes uint64
// Extra metrics collected by ReportMetric.
extra map[string]float64
}
可以看到,testing.B 的首个元素也是 common 结构,因此他也拥有 testing.T 中所有的报告方法,可以参考上一篇文章的讲解: 测试驱动开发与 golang 单元测试
4. 并行测试
既然是性能压测,串行的执行统计运行耗时常常并不是我们想要的测试手段,通过并发执行来观察资源的消耗情况是更好的测试方法。 go test 提供了 b.RunParallel 方法用来实现让多个基准测试方法并行执行的功能。
代码语言:javascript复制func BenchmarkTemplateParallel(b *testing.B) {
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")
}
})
}
调用了 b.RunParallel 方法的测试函数将会在单独的 goroutine 中启动。 需要注意的是,b.StartTimer、b.StopTime、b.ResetTimer 三个方法会影响到所有 goroutine,因此不要在并行测试中调用。
4.1. 调节并发度
并行基准测试其并发度受环境变量 GOMAXPROCS 控制,默认情况下是 CPU 核心数。 可以在测试开始前,通过 b.SetParallelism 方法实现对并发度的控制,例如执行b.SetParallelism(2) 则意味着并发度为 2*GOMAXPROCS。 在执行 go test 命令时,增加 -cpu 参数,可以打印 cpu 资源消耗的详情信息。
5. 内存统计
除了观察 CPU 的调用情况,我们也常常需要去观察内存的使用情况。 通过 go test 命令添加 -benchmem 参数可以打开基准测试的内存统计功能。 也可以通过在测试用例执行开始前,调用 b.ReportAllocs 函数,这样做的好处是只会影响你需要的函数:
代码语言:javascript复制func BenchmarkTmplExucte(b *testing.B) {
b.ReportAllocs()
templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
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")
}
})
}
打印出了:
BenchmarkTmplExucte-4 2000000 898 ns/op 368 B/op 9 allocs/op
表示总计执行 2000000 次,平均每次耗时 898 纳秒,每次执行消耗内存 368 Bytes,共计分配内存 9 次。
6. 并发安全测试 — -race
在介绍 goroutine 并发安全时,我们曾经介绍了并发安全测试相关的内容: goroutine 并发中竞争条件的解决
只要在 go test 命令中加入 -race 参数,就可以在测试阶段发现可能的并发安全问题。 下面是一个典型的非并发安全的例子:
代码语言:javascript复制func TestParallelSafe(t *testing.T) {
a := 1
go func(){
a = 2
}()
a = 3
t.Logf("a is ", a)
time.Sleep(2 * time.Second)
}
执行 go test -race . 会打印出:
runtime go test -race . a is 3 ================== WARNING: DATA RACE Write by goroutine 5: main.func·001() /data/test/race1.go:11 0x3a Previous write by main goroutine: main.main() /data/test/race1.go:13 0xe7 Goroutine 5 (running) created at: main.main() /data/test/race1.go:12 0xd7 ================== Found 1 data race(s) exit status 66
从而可以发现竞争条件的存在。 但需要注意的是,只有测试用例覆盖到的代码才可以顺利检测出竞争,因此保证测试用例的覆盖率是一个很重要的事。
6.1. 打印测试用例覆盖率报告
go test 命令增加 -coverprofile 参数,指定输出文件,就可以输出测试的覆盖率报告。