這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
介紹
在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後跟以'/'分割的Regex,用來制定特定的測試案例。
- 執行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 mainimport ( "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/opBenchmarkProductInfo/p7-4 10000 108921 ns/opBenchmarkProductInfo/p666-4 10000 107163 ns/opBenchmarkProductInfoParalle-4 10000 113386 ns/opBenchmarkProductLock/pn3-4 10000 100418 ns/opBenchmarkProductLock/p7-4 20000 97373 ns/opBenchmarkProductLock/p666-4 20000 96905 ns/opBenchmarkProductLockParallel-4 10000 108399 ns/op
執行如下測試命令
go test -bench=ProductInfo
過濾測試函數名中包含ProductInfo的測試案例,結果:
BenchmarkProductInfo/pn3-4 10000 111065 ns/opBenchmarkProductInfo/p7-4 10000 118515 ns/opBenchmarkProductInfo/p666-4 10000 111723 ns/opBenchmarkProductInfoParalle-4 10000 118641 ns/op
執行如下測試命令
go test -bench=oductInfo
過濾測試函數名中包含oductInfo的測試案例,結果:
BenchmarkProductInfo/pn3-4 10000 107338 ns/opBenchmarkProductInfo/p7-4 10000 109848 ns/opBenchmarkProductInfo/p666-4 10000 109344 ns/opBenchmarkProductInfoParalle-4 10000 114351 ns/op
執行如下測試命令
go test -bench=ProductInfo/p7
過濾測試函數名中包含ProductInfo且子測試名稱包含p7的測試案例,同時我們可以注意到並行的測試也執行了。結果:
BenchmarkProductInfo/p7-4 10000 109045 ns/opBenchmarkProductInfoParalle-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") } }) }}