golang 1.7之后高级测试方法之子测试,子基准测试(sub
介绍
在go1.7之后,testing包T和B的引入了一个Run方法,用于创建subtests 和 sub-benchmarks. subtests 和 sub-benchmarks可以让开发者更好的处理测试中的失败,更好的控制运行哪个测试用例,控制并行测试操作,测试代码更加简洁和可维护性更强。
Table-driven tests 基础
首先我们先讨论下Go中常见的测试代码编写方式。
一系列相关的测试校验可以通过遍历测试用例的切片来实现,代码如下:
func TestTime(t *testing.T) {
testCases := []struct {
gmt string
loc string
want string
}{
{"12:31", "Europe/Zuri", "13:31"}, // incorrect location name
{"12:31", "America/New_York", "7:31"}, // should be 07:31
{"08:08", "Australia/Sydney", "18:08"},
}
for _, tc := range testCases {
loc, err := time.LoadLocation(tc.loc)
if err != nil {
t.Fatalf("could not load location %q", tc.loc)
}
gmt, _ := time.Parse("15:04", tc.gmt)
if got := gmt.In(loc).Format("15:04"); got != tc.want {
t.Errorf("In(%s, %s) = %s; want %s", tc.gmt, tc.loc, got, tc.want)
}
}
}
测试函数必须以Test开头,Test后跟的名字也必须首字母大写。
上面的测试方式称为table-driven 测试法,可以降低重复代码。
Table-driven benchmarks
在go1.7之前是不能够对benchmarks采用table-driven的方法的,如果要测试不同的参数就需要编写不同的benchmark函数,在go1.7之前常见的benchmarks测试代码如下:
func benchmarkAppendFloat(b *testing.B, f float64, fmt byte, prec, bitSize int) {
dst := make([]byte, 30)
b.ResetTimer() // Overkill here, but for illustrative purposes.
for i := 0; i < b.N; i++ {
AppendFloat(dst[:0], f, fmt, prec, bitSize)
}
}
func BenchmarkAppendFloatDecimal(b *testing.B) { benchmarkAppendFloat(b, 33909, 'g', -1, 64) }
func BenchmarkAppendFloat(b *testing.B) { benchmarkAppendFloat(b, 339.7784, 'g', -1, 64) }
func BenchmarkAppendFloatExp(b *testing.B) { benchmarkAppendFloat(b, -5.09e75, 'g', -1, 64) }
func BenchmarkAppendFloatNegExp(b *testing.B) { benchmarkAppendFloat(b, -5.11e-95, 'g', -1, 64) }
func BenchmarkAppendFloatBig(b *testing.B) { benchmarkAppendFloat(b, 123456789123456789123456789, 'g', -1, 64) }
go1.7之后,采用table-drive方法代码如下:
func BenchmarkAppendFloat(b *testing.B) {
benchmarks := []struct{
name string
float float64
fmt byte
prec int
bitSize int
}{
{"Decimal", 33909, 'g', -1, 64},
{"Float", 339.7784, 'g', -1, 64},
{"Exp", -5.09e75, 'g', -1, 64},
{"NegExp", -5.11e-95, 'g', -1, 64},
{"Big", 123456789123456789123456789, 'g', -1, 64},
...
}
dst := make([]byte, 30)
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
AppendFloat(dst[:0], bm.float, bm.fmt, bm.prec, bm.bitSize)
}
})
}
}
每个b.Run单独创建一个benchmark。
可以看到新的编码方式可读性和可维护行上更强。
如果想要子测试并发执行,则使用 b.RunParallel
Table-driven tests using subtests
Go1.7之后引用Run方法用于创建subtests,对之前 Table-driven tests 基础 中的代码重新写为:
func TestTime(t *testing.T) {
testCases := []struct {
gmt string
loc string
want string
}{
{"12:31", "Europe/Zuri", "13:31"},
{"12:31", "America/New_York", "7:31"},
{"08:08", "Australia/Sydney", "18:08"},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%s in %s", tc.gmt, tc.loc), func(t *testing.T) {
loc, err := time.LoadLocation(tc.loc)
if err != nil {
t.Fatal("could not load location")
}
gmt, _ := time.Parse("15:04", tc.gmt)
if got := gmt.In(loc).Format("15:04"); got != tc.want {
t.Errorf("got %s; want %s", got, tc.want)
}
})
}
}
go1.7之前的 Table-driven tests 基础 的测试代码运行结果为:
--- FAIL: TestTime (0.00s)
time_test.go:62: could not load location "Europe/Zuri"
虽然两个用例都是错误的,但是 第一个用例Fatalf 后,后面的用例也就没能进行运行。
使用Run的测试代码运行结果为:
--- FAIL: TestTime (0.00s)
--- FAIL: TestTime/12:31_in_Europe/Zuri (0.00s)
time_test.go:84: could not load location
--- FAIL: TestTime/12:31_in_America/New_York (0.00s)
time_test.go:88: got 07:31; want 7:31
Fatal 导致subtest被跳过,不过不影响其他subtest以及父test的测试。
针对每一个子测试,go test命令都会打印出一行测试摘要。它们是分离的、独立统计的。这可以让我们进行更加精细的测试,细到每次输入输出。
过滤执行测试用例
subtests和sub-benchmarks可以使用 -run or -bench flag
来对测试用例进行过滤运行。 -run or -bench flag后跟以'/'分割的正则表达式,用来制定特定的测试用例。
- 执行TestTime下匹配"in Europe" 的子测试
$ go test -run=TestTime/"in Europe"
--- FAIL: TestTime (0.00s)
--- FAIL: TestTime/12:31_in_Europe/Zuri (0.00s)
time_test.go:85: could not load location
- 执行TestTime下匹配"12:[0-9] " 的子测试
$ go test -run=Time/12:[0-9] -v
=== RUN TestTime
=== RUN TestTime/12:31_in_Europe/Zuri
=== RUN TestTime/12:31_in_America/New_York
--- FAIL: TestTime (0.00s)
--- FAIL: TestTime/12:31_in_Europe/Zuri (0.00s)
time_test.go:85: could not load location
--- FAIL: TestTime/12:31_in_America/New_York (0.00s)
time_test.go:89: got 07:31; want 7:31
$ go test -run=Time//New_York
--- FAIL: TestTime (0.00s)
--- FAIL: TestTime/12:31_in_America/New_York (0.00s)
time_test.go:88: got 07:31; want 7:31
func (*T) Parallel
func (t *T) Parallel()
使用t.Parallel(),使测试和其它子测试并发执行。
tc := tc这个地方很关键,不然多个子测试可能使用的tc是同一个。
func TestGroupedParallel(t *testing.T) {
for _, tc := range testCases {
tc := tc // capture range variable
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
if got := foo(tc.in); got != tc.out {
t.Errorf("got %v; want %v", got, tc.out)
}
...
})
}
}
func (*B) RunParallel
func (b *B) RunParallel(body func(*PB))
RunParallel runs a benchmark in parallel. It creates multiple goroutines and distributes b.N iterations among them. The number of goroutines defaults to GOMAXPROCS. To increase parallelism for non-CPU-bound benchmarks, call SetParallelism before RunParallel. RunParallel is usually used with the go test -cpu flag.
The body function will be run in each goroutine. It should set up any goroutine-local state and then iterate until pb.Next returns false. It should not use the StartTimer, StopTimer, or ResetTimer functions, because they have global effect. It should also not call Run.
RunParallel并发的执行benchmark。RunParallel创建多个goroutine然后把b.N个迭代测试分布到这些goroutine上。goroutine的数目默认是GOMAXPROCS。如果要增加non-CPU-bound的benchmark的并个数,在执行RunParallel之前调用SetParallelism。
不要使用 StartTimer, StopTimer, or ResetTimer functions这些函数,因为这些函数都是 global effect的。
package main
import (
"bytes"
"testing"
"text/template"
)
func main() {
// Parallel benchmark for text/template.Template.Execute on a single object.
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")
}
})
})
}
本人测试实例
Benchmark测试代码
func BenchmarkProductInfo(b *testing.B) {
// b.ResetTimer()
testCases := []string{"pn3", "p7", "p666"}
for _, productId := range testCases {
// b.SetParallelism
b.Run(productId, func(b *testing.B) {
for i := 0; i < b.N; i++ {
mgoDB.ecnGetProductInfoOfProductId(productId)
}
})
}
}
func BenchmarkProductInfoParalle(b *testing.B) {
// b.ResetTimer()
testCases := []string{"pn3", "p7", "p666"}
for _, tproductId := range testCases {
// b.SetParallelism
productId := tproductId
b.RunParallel(func(b *testing.PB) {
for b.Next() {
mgoDB.ecnGetProductInfoOfProductId(productId)
}
})
}
}
func BenchmarkProductLock(b *testing.B) {
// b.ResetTimer()
testCases := []string{"pn3", "p7", "p666"}
for _, productId := range testCases {
// b.SetParallelism
b.Run(productId, func(b *testing.B) {
for i := 0; i < b.N; i++ {
mgoDB.CheckProductLockStatus(productId)
}
})
}
}
func BenchmarkProductLockParallel(b *testing.B) {
// b.ResetTimer()
testCases := []string{"pn3", "p7", "p666"}
for _, tproductId := range testCases {
// b.SetParallelism
productId := tproductId
b.RunParallel(func(b *testing.PB) {
for b.Next() {
mgoDB.CheckProductLockStatus(productId)
}
})
}
}
- 执行如下测试命令
go test -bench="."
结果
BenchmarkProductInfo/pn3-4 10000 107704 ns/op
BenchmarkProductInfo/p7-4 10000 108921 ns/op
BenchmarkProductInfo/p666-4 10000 107163 ns/op
BenchmarkProductInfoParalle-4 10000 113386 ns/op
BenchmarkProductLock/pn3-4 10000 100418 ns/op
BenchmarkProductLock/p7-4 20000 97373 ns/op
BenchmarkProductLock/p666-4 20000 96905 ns/op
BenchmarkProductLockParallel-4 10000 108399 ns/op
- 执行如下测试命令
go test -bench=ProductInfo
过滤测试函数名中包含ProductInfo的测试用例,结果:
BenchmarkProductInfo/pn3-4 10000 111065 ns/op
BenchmarkProductInfo/p7-4 10000 118515 ns/op
BenchmarkProductInfo/p666-4 10000 111723 ns/op
BenchmarkProductInfoParalle-4 10000 118641 ns/op
- 执行如下测试命令
go test -bench=oductInfo
过滤测试函数名中包含oductInfo的测试用例,结果:
BenchmarkProductInfo/pn3-4 10000 107338 ns/op
BenchmarkProductInfo/p7-4 10000 109848 ns/op
BenchmarkProductInfo/p666-4 10000 109344 ns/op
BenchmarkProductInfoParalle-4 10000 114351 ns/op
- 执行如下测试命令
go test -bench=ProductInfo/p7
过滤测试函数名中包含ProductInfo且子测试名称包含p7的测试用例,同时我们可以注意到并行的测试也执行了。结果:
BenchmarkProductInfo/p7-4 10000 109045 ns/op
BenchmarkProductInfoParalle-4 10000 117569 ns/op
Test测试代码
func TestCheckProductLockt(t *testing.T) {
testCases := []string{"a1", "a2", "a3"}
for _, productID := range testCases {
t.Log(productID)
t.Run(productID, func(t *testing.T) {
_, ret := mgoDB.ecnGetProductInfoOfProductId(productID)
if ret != Success {
t.Fatalf("faield")
}
})
}
}
func TestCheckProductLocktParalle(t *testing.T) {
testCases := []string{"a1", "a2", "a3"}
for _, tproductID := range testCases {
productID := tproductID
t.Log(productID)
t.Run(productID, func(t *testing.T) {
t.Parallel()
_, ret := mgoDB.ecnGetProductInfoOfProductId(productID)
if ret != Success {
t.Fatalf("faield")
}
})
}
}
func TestUserIDMatchRole(t *testing.T) {
reqData := []struct {
ProductID string
UserID string
RoleType string
}{
{"pn2", "48176d26e860975e96518b80a3520407", "HR"},
{"pn2", "48176d26e860975e96518b80a3520407", "CEO"},
{"pn2", "48176d26e860975e96518b80a3520407", "CTO"},
}
for _, data := range reqData {
//
t.Log(data)
t.Run(fmt.Sprint("%s %s", data.ProductID, data.RoleType), func(t *testing.T) {
if ret := checkUserMatchProductRole(data.ProductID, data.UserID, data.RoleType); ret != Success {
t.Error("not match")
}
})
}
}
func TestUserIDMatchRoleParall(t *testing.T) {
reqData := []struct {
ProductID string
UserID string
RoleType string
}{
{"pn2", "48176d26e860975e96518b80a3520407", "HR"},
{"pn2", "48176d26e860975e96518b80a3520407", "CEO"},
{"pn2", "48176d26e860975e96518b80a3520407", "CTO"},
}
for _, tdata := range reqData {
//
data := tdata //重要
t.Log(data)
t.Run(fmt.Sprint("%s %s", data.ProductID, data.RoleType), func(t *testing.T) {
t.Parallel()
if ret := checkUserMatchProductRole(data.ProductID, data.UserID, data.RoleType); ret != Success {
t.Error("not match")
}
})
}
}
- 执行如下测试命令
go test -bench="."
结果
--- FAIL: TestCheckProductLockt (0.00s)
ecn_test.go:626: a1
--- FAIL: TestCheckProductLockt/a1 (0.00s)
ecn_test.go:630: faield
ecn_test.go:626: a2
--- FAIL: TestCheckProductLockt/a2 (0.00s)
ecn_test.go:630: faield
ecn_test.go:626: a3
--- FAIL: TestCheckProductLockt/a3 (0.00s)
ecn_test.go:630: faield
--- FAIL: TestCheckProductLocktParalle (0.00s)
ecn_test.go:642: a1
ecn_test.go:642: a2
ecn_test.go:642: a3
--- FAIL: TestCheckProductLocktParalle/a1 (0.00s)
ecn_test.go:647: faield
--- FAIL: TestCheckProductLocktParalle/a2 (0.00s)
ecn_test.go:647: faield
--- FAIL: TestCheckProductLocktParalle/a3 (0.00s)
ecn_test.go:647: faield
--- FAIL: TestUserIDMatchRole (0.00s)
ecn_test.go:668: {pn2 48176d26e860975e96518b80a3520407 HR}
--- FAIL: TestUserIDMatchRole/%s_%spn2HR (0.00s)
ecn_test.go:671: not match
ecn_test.go:668: {pn2 48176d26e860975e96518b80a3520407 CEO}
--- FAIL: TestUserIDMatchRole/%s_%spn2CEO (0.00s)
ecn_test.go:671: not match
ecn_test.go:668: {pn2 48176d26e860975e96518b80a3520407 CTO}
--- FAIL: TestUserIDMatchRole/%s_%spn2CTO (0.00s)
ecn_test.go:671: not match
--- FAIL: TestUserIDMatchRoleParall (0.00s)
ecn_test.go:692: {pn2 48176d26e860975e96518b80a3520407 HR}
ecn_test.go:692: {pn2 48176d26e860975e96518b80a3520407 CEO}
ecn_test.go:692: {pn2 48176d26e860975e96518b80a3520407 CTO}
--- FAIL: TestUserIDMatchRoleParall/%s_%spn2HR (0.00s)
ecn_test.go:696: not match
--- FAIL: TestUserIDMatchRoleParall/%s_%spn2CTO (0.00s)
ecn_test.go:696: not match
--- FAIL: TestUserIDMatchRoleParall/%s_%spn2CEO (0.00s)
ecn_test.go:696: not match
在测试代码中我们添加了t.log的打印,通过打印对比并发版本和非并发版本的输出,可以看到非并发版本的测试的确时顺序执行的,而并发版本的测试是并发执行的。