Go Test測試簡介
Go語言擁有一套單元測試和性能測試系統(tǒng),僅需要添加很少的代碼就可以快速測試一段需求代碼。
go test命令,會自動讀取源碼目錄下面名為 *_test.go 的文件,生成并運行測試用的可執(zhí)行文件并輸出結(jié)果信息。本文中測試用例來源于golang官方文檔并且測試結(jié)果為golang 1.12.5下完成
測試類型
go test的測試類型分為以下三種
-
單元測試,測試函數(shù)需要以
Test為前綴,例如:func TestXxx(*testing.T)注意:Xxx 可以是任何字母數(shù)字字符串,但是第一個字母不能是小些字母。
在這些函數(shù)中,使用 Error, Fail 或相關(guān)方法來發(fā)出失敗信號。
要編寫一個新的測試套件,需要創(chuàng)建一個名稱以 _test.go 結(jié)尾的文件,該文件包含
TestXxx函數(shù),如上所述。 將該文件放在與被測試的包相同的包中。該文件將被排除在正常的程序包之外,但在運行 “ go test ” 命令時將被包含。 有關(guān)詳細(xì)信息,請運行 “ go help test ” 和 “ go help testflag ” 了解。如果有需要,可以調(diào)用
*T和*B的 Skip 方法,跳過該測試或基準(zhǔn)測試:func TestTimeConsuming(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } // ... }注意:
testing.Short()判斷是否-test.short flag是否被設(shè)置(后續(xù)示例會進行演示) -
基準(zhǔn)測試,測試函數(shù)需要以
Benchmark為前綴,例如:func BenchmarkXxx(*testing.B)通過 "go test" 命令,加上
-benchflag 來執(zhí)行,多個基準(zhǔn)測試按照順序運行。基準(zhǔn)測試函數(shù)如下所示:
func BenchmarkHelloWorld(b *testing.B) { for i := 0; i < b.N; i++ { fmt.Sprintf("Hello World") } }基準(zhǔn)函數(shù)會運行目標(biāo)代碼 b.N 次。在基準(zhǔn)執(zhí)行期間,會調(diào)整 b.N 直到基準(zhǔn)測試函數(shù)持續(xù)足夠長的時間
BenchmarkHelloWorld 1000000 216 ns/op意味著次基準(zhǔn)測試函數(shù)在測試時間內(nèi)循環(huán)執(zhí)行了 1000000 次,每次循環(huán)花費 2216 納秒 (ns)。
注意:為了縮短基準(zhǔn)測試,請使用-benchtime命令行標(biāo)志設(shè)置基準(zhǔn)測試運行時間,而不要使用
testing.short()函數(shù)跳過部分基準(zhǔn)測試。 -
子測試
單元測試和基準(zhǔn)測試的Run方法允許定義子測試和子基準(zhǔn)測試,而不必為每個子函數(shù)定義單獨的功能。 這使得諸如
table-driven測試以及creating hierarchical成為了可能。 它還提供了一種共享通用設(shè)置和拆卸代碼的方法:func TestFoo(t *testing.T) { // <setup code> 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") }) // <tear-down code> }每個子測試和子基準(zhǔn)測試都有一個唯一的名稱:頂級測試的名稱和傳遞給 Run 的名稱的組合,以斜杠分隔,并具有用于消歧的可選尾隨序列號。
code示例&執(zhí)行解析
待測試的函數(shù)代碼
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) } }執(zhí)行
go test .輸出結(jié)果$ go test . ok _/Users/xxxxx/Workspace/test/src/codetest 0.005s`此結(jié)果表示測試通過。
我們可以在文件
fib_test.go新增一個測試函數(shù)TestFib1func 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) } }執(zhí)行
go test .輸出結(jié)果$ 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測試函數(shù)失敗進而導(dǎo)致整體的單元測試失敗。運行指定單元測試用例
go test指定文件時默認(rèn)執(zhí)行文件內(nèi)的所有測試用例??梢允褂?code>-run參數(shù)選擇需要的測試用例單獨執(zhí)行,參考一下的代碼。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") }當(dāng)指定只執(zhí)行TestCodeA開頭的函數(shù)時,可以使用如下命令:
go test -v -run TestCodeA或go test -v -run ^TestCodeA[a-zA-Z0-9]*$輸出結(jié)果如下:
=== 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.005sTable-driven覆蓋測試
當(dāng)在測試中需要進行多case的覆蓋測試,如果每次都以修改單元測試函數(shù)中的方式實現(xiàn),這樣無疑顯得很笨拙。在此情況下,我們可以采用 Table-driven 的方式寫測試。我們在
lib_test.go中新增一個測試函數(shù)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 失敗,并不會終止測試執(zhí)行。如下所示:將
TestFibMore函數(shù)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 -
基準(zhǔn)測試
func BenchmarkFib(b *testing.B) { for n := 0; n < b.N; n++ { Fib(10) } }運行指定基準(zhǔn)測試用例
若單元測試函數(shù)與基準(zhǔn)測試函數(shù)在同一包下時或同一個文件時,可以通過在命令中添加
-run=none或者-run=^$來規(guī)避掉單元測試的運行運行測試命令
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指定運基準(zhǔn)測試運行時間
我們還可以通過在命令中加入
-benchtime=60m來設(shè)置基準(zhǔn)測試的運行時間。$ 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嵌套基準(zhǔn)測試函數(shù)
同時我們還可以通過創(chuàng)建一個基礎(chǔ)函數(shù)并在此基礎(chǔ)上衍生出更多基準(zhǔn)測試的方法來進行多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應(yīng)為系統(tǒng)的最大核心數(shù)內(nèi)存統(tǒng)計
在運行相關(guān)基準(zhǔn)測試時,可以使用
-benchmem參數(shù)查看基準(zhǔn)測試的內(nèi)存使用情況。$ 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從運行結(jié)果中看出在測試結(jié)果中增加了兩列數(shù)據(jù)分別為
xx B/op與xx allocs/op,分別代表在-一次循環(huán)中分配的內(nèi)存大小和一次循環(huán)中進行了多少次內(nèi)存的分配。計時器的使用
-
StartTimer()
開始計時測試。在基準(zhǔn)測試開始之前會自動調(diào)用此函數(shù),但也可以用于在調(diào)用StopTimer之后恢復(fù)計時。
-
StopTimer()
停止計時測試。 這可用于在執(zhí)行您不想測量的復(fù)雜初始化時暫停計時器。
-
ResetTimer()
將經(jīng)過的基準(zhǔn)時間和內(nèi)存分配計數(shù)器歸零,并刪除用戶報告的指標(biāo)。 它不影響計時器是否正在運行。
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函數(shù)的定義,推測只是代表耗時操作吧。并行執(zhí)行基準(zhǔn)測試
如果基準(zhǔn)測試需要在并行設(shè)置中測試性能,則可以使用 RunParallel 輔助函數(shù) ; 這樣的基準(zhǔn)測試一般與
go test -cpu標(biāo)志一起使用。RunParallel 會創(chuàng)建出多個 goroutine,并將 b.N 分配給這些 goroutine 執(zhí)行,其中 goroutine 數(shù)量的默認(rèn)值為 GOMAXPROCS。如果想要增加goroutine的數(shù)量,可以調(diào)用函數(shù)SetParallelism將RunParallel使用的goroutine的數(shù)量設(shè)置為p * GOMAXPROCS。RunParallel函數(shù)將在每個 goroutine 中執(zhí)行,這個函數(shù)需要設(shè)置所有 goroutine 本地的狀態(tài),并迭代直到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 一起,循環(huán)一共執(zhí)行 b.N 次 buf.Reset() templ.Execute(&buf, "World") } }) }運行結(jié)果
$ 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基準(zhǔn)測試結(jié)果
BenchmarkResult結(jié)構(gòu)包含了基準(zhǔn)測試運行后的結(jié)果。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()) }運行結(jié)果如下所示:
$ go run main.go 30000000 46.0 ns/op 48 B/op 1 allocs/op具體函數(shù)定義及解釋可以參考golang BenchmarkResult
-
-
子測試
package codetest import ( "fmt" "testing" ) func TestFoo(t *testing.T) { // <setup code> 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") }) // <tear-down code> } func TestUoo(t *testing.T) { // <setup code> t.Run("U=1", func(t *testing.T) { fmt.Println("U=1") }) t.Run("A=1", func(t *testing.T) { fmt.Println("A=1") }) // <tear-down code> }-run和-bench命令行標(biāo)志的參數(shù)是與測試名稱相匹配的非固定的正則表達式。對于具有多個斜杠分隔元素(例如子測試)的測試,該參數(shù)本身是斜杠分隔的,其中表達式依次匹配每個名稱元素。因為它是非固定的,一個空的表達式匹配任何字符串。例如,使用 " 匹配 " 表示 " 其名稱包含 ":$ 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子測試也可用于控制并行性。所有的子測試完成后,父測試才會完成。在這個例子中,所有的測試是相互并行運行的,當(dāng)然也只是彼此之間,不包括定義在其他頂層測試的子測試:
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) }) } }執(zhí)行命令
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) }) // <tear-down code> } 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).出現(xiàn)此種類型的錯誤是因為golang test存在默認(rèn)的超時間為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參數(shù),參數(shù)接受s、m、h等級別的時間參數(shù)