在Go中編寫測試代碼的方法總結(jié)
測試分類
在 Go 中,編寫的測試用例可以分為四類:
單元測試:測試函數(shù)名稱以
Test
開頭,如TestXxx
、Test_Xxx
,用來對程序的最小單元進(jìn)行測試,如函數(shù)、方法等。基準(zhǔn)測試:也叫性能測試,測試函數(shù)名稱以
Benchmark
開頭,用來測量程序的性能指標(biāo)。示例測試:測試函數(shù)名稱以
Example
開頭,可以用來測試程序的標(biāo)準(zhǔn)輸出內(nèi)容。模糊測試:也叫隨機(jī)測試,測試函數(shù)名稱以
Fuzz
開頭,是一種基于隨機(jī)輸入的自動化測試技術(shù),適合用來測試處理用戶輸入的代碼,在 Go 1.18 中被引入。
這四類測試用例都有各自的適用場景,其中單元測試最為常用,你一定要掌握。
以上這些測試用例都可以使用 go test
命令來執(zhí)行。
測試規(guī)范
編寫測試代碼并不需要我們學(xué)習(xí)新的 Go 語法,但有些測試規(guī)范還是需要遵守的。
Go 在提供 testing
測試框架時(shí),就規(guī)定了很多測試規(guī)范,用來約束我們編寫測試的方式,這有助于項(xiàng)目的工程化。
測試文件命名規(guī)范
首先,測試文件命名必須以 _test.go
結(jié)尾,否則將被測試框架忽略。比如我們的 Go 代碼文件名為 hello.go
,則測試文件可以命名為 hello_test.go
。
只有以 _test.go
結(jié)尾的測試文件,才能使用 go test
命令執(zhí)行。
在構(gòu)建 Go 程序時(shí),go build
命令會忽略以 _test.go
結(jié)尾的測試文件。
測試包命名規(guī)范
測試用例除了根據(jù)使用場景可以分為四類,還可以根據(jù)代碼和測試用例是否在一個(gè)包中,分為白盒測試和黑盒測試。
白盒測試:將測試代碼和被測代碼放在同一個(gè)包中,也就是二者包名相同,這些測試用例屬于白盒測試。比如 Go 代碼文件名為
hello.go
,包名為hello
,測試文件可以命名為hello_test.go
,并且必須與hello.go
放在同一個(gè)目錄下,包名也必須為hello
。白盒測試的測試用例可以使用和測試當(dāng)前包中所有標(biāo)識符(變量、函數(shù)等),包括未導(dǎo)出的標(biāo)識符。黑盒測試:將測試代碼和被測代碼放在不同的包中,即包名不同,這些測試用例屬于黑盒測試。比如 Go 代碼文件名為
hello.go
,包名為hello
,測試文件同樣可以命名為hello_test.go
,與hello.go
放在同一個(gè)目錄下,但包名不能再叫hello
,應(yīng)該命名為hello_test
,hello_test.go
文件也可以放在專門的test
目錄下,此時(shí)可以隨意命名包名。黑盒測試的測試用例僅能夠使用和測試被測代碼包中可導(dǎo)出的標(biāo)識符,因?yàn)槎咭呀?jīng)不再屬于同一個(gè)包,這遵循 Go 語法規(guī)范。
根據(jù)二者各自特點(diǎn),在開發(fā)時(shí)我們應(yīng)該多編寫白盒測試,這樣才能提升代碼測試覆蓋率。
測試用例命名規(guī)范
在 Go 中我們使用測試函數(shù)來編寫測試用例,根據(jù)單元測試、基準(zhǔn)測試、示例測試、模糊測試四種不同類型的測試分類,測試函數(shù)必須以 Test
、Benchmark
、Example
、Fuzz
其中一種開頭。
測試函數(shù)簽名示例如下:
func TestXxx(*testing.T) func BenchmarkXxx(*testing.B) func ExampleXxx() func FuzzXxx(*testing.F)
測試函數(shù)不能有返回值,其中單元測試、基準(zhǔn)測試和模糊測試都接收一個(gè)參數(shù),由 testing
框架提供,示例測試則不需要傳遞參數(shù)。
其中 Xxx
一般是被測試的函數(shù)名稱,首字母必須大寫。如果是以 Test_Xxx
方式命名測試函數(shù),則 Xxx
首字母大小寫均可。
測試變量命名規(guī)范
對于測試變量的命名,testing
框架沒有有強(qiáng)制約束,但社區(qū)中也形成了一些規(guī)范。
比如,函數(shù)簽名中的參數(shù)變量定義如下:
func TestXxx(t *testing.T) func BenchmarkXxx(b *testing.B) func FuzzXxx(f *testing.F)
單元測試、基準(zhǔn)測試和模糊測試參數(shù)變量即為參數(shù)類型 *testing.<T>
的小寫形式。
在編寫測試代碼時(shí),有一個(gè)最常見的場景,就是比較被測函數(shù)的實(shí)際輸出和測試函數(shù)中的預(yù)期輸出是否相等,通常可以使用 got/want
或 actual/expected
來命名變量:
if got != want { t.Errorf("Xxx(x) = %s; want %s", got, want) }
或:
if actual != expected { t.Errorf("Xxx(x) = %s; expected %s", actual, expected) }
讀者可以根據(jù)喜好和團(tuán)隊(duì)中的開發(fā)規(guī)范選擇其中一種變量命名。
此外,在單元測試中我們還會經(jīng)常編寫一種叫表格測試的測試用例,寫法如下:
func TestXxx(t *testing.T) { tests := []struct { name string arg float64 want float64 }{ ... } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := Xxx(tt.arg); got != tt.want { t.Errorf("Xxx(%f) = %v, want %v", tt.arg, got, tt.want) } }) } }
其中 tests
代表多個(gè)測試用例,循環(huán)時(shí)以 tt
作為循環(huán)變量(tt
可以避免與單元測試函數(shù)的參數(shù)變量 t
命名沖突)。
表格測試還有另一個(gè)版本:
func TestXxx(t *testing.T) { cases := []struct { name string arg float64 want float64 }{ ... } for _, cc := range cases { t.Run(cc.name, func(t *testing.T) { if got := Xxx(cc.arg); got != cc.want { t.Errorf("Xxx(%f) = %v, want %v", cc.arg, got, cc.want) } }) } }
現(xiàn)在 cases
代表多個(gè)測試用例,循環(huán)時(shí)以 cc
作為循環(huán)變量(cc
可以避免與常見的 context
縮寫 c
命名沖突)。
編寫測試代碼的常見規(guī)范我們就先講解到這里,更多規(guī)范將在下文講解對應(yīng)示例時(shí)再進(jìn)行詳細(xì)說明。
單元測試
單元測試是我們最常編寫的測試用例,所以先來學(xué)習(xí)下如何編寫單元測試。
首先,我們準(zhǔn)備一個(gè) Abs
函數(shù)作為被測試的代碼,存放于 abs.go
文件中,其包名為 abs
,代碼如下:
package abs import "math" func Abs(x float64) float64 { return math.Abs(x) }
白盒測試
現(xiàn)在為 Abs
編寫一個(gè)白盒測試函數(shù),在存放 abs.go
文件的同一目錄下,新建 abs_test.go
文件,包名同樣定義為 abs
,編寫測試代碼如下:
package abs import "testing" func TestAbs(t *testing.T) { got := Abs(-1) if got != 1 { t.Errorf("Abs(-1) = %f; want 1", got) } }
單元測試函數(shù) TestAbs
代碼非常簡單,先調(diào)用了 Abs(-1)
函數(shù),并將得到的返回結(jié)果 got
與 1
做相等性比較,如果不相等,則說明測試沒有通過,使用 t.Errorf
打印錯(cuò)誤信息。
參數(shù) *testing.T
是一個(gè)結(jié)構(gòu)體指針,提供了如下幾個(gè)方法用于錯(cuò)誤報(bào)告:
t.Log/t.Logf
:打印正常日志信息,類似fmt.Print
。t.Error/t.Errorf
:打印測試失敗時(shí)的錯(cuò)誤信息,不影響當(dāng)前測試函數(shù)內(nèi)后續(xù)代碼的繼續(xù)執(zhí)行。t.Fatal/t.Fatalf
:打印測試失敗時(shí)的錯(cuò)誤信息,并終止當(dāng)前測試函數(shù)執(zhí)行。
在測試函數(shù)所在目錄下使用 go test
命令執(zhí)行測試代碼:
$ go test PASS ok github.com/jianghushinian/blog-go-example/test/getting-started/abs 0.139s
go test
會自動查找當(dāng)前目錄下所有以 _test.go
結(jié)尾來命名的測試文件,并執(zhí)行其內(nèi)部編寫的全部測試函數(shù)。
輸出 PASS
表示測試通過,github.com/jianghushinian/blog-go-example/test/getting-started/abs
是程序的 module
名稱。
go test
命令還支持使用 -v
標(biāo)志輸出更多信息:
$ go test -v === RUN TestAbs --- PASS: TestAbs (0.00s) PASS ok github.com/jianghushinian/blog-go-example/test/getting-started/abs 0.437s
如果我們不小心將單元測試的函數(shù)名錯(cuò)誤的寫成 Testabs
,即 abs
沒有大寫開頭:
func Testabs(t *testing.T) { got := Abs(-1) if got != 1 { t.Errorf("Abs(-1) = %f; want 1", got) } }
則測試函數(shù)不會被 go test
命令執(zhí)行。
通常情況下,我們不會對一個(gè)函數(shù)只做一種輸入?yún)?shù)的測試,為了提高測試覆蓋率,我們可能還需要多測試幾種參數(shù)的用例,比如測試下 Abs(2)
、Abs(3)
等是否正確。
這時(shí),可以像如下這樣編寫測試函數(shù):
func TestAbs(t *testing.T) { got := Abs(-1) if got != 1 { t.Errorf("Abs(-1) = %f; want 1", got) } got = Abs(2) if got != 2 { t.Errorf("Abs(2) = %f; want 2", got) } }
但這樣的代碼顯然過于“平鋪直敘”,不夠優(yōu)雅。
在這種更加復(fù)雜的情況下,我們可以使用「表格測試」,代碼如下:
func TestAbs_TableDriven(t *testing.T) { tests := []struct { name string x float64 want float64 }{ { name: "positive", x: 2, want: 2, }, { name: "negative", x: -3, want: 3, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := Abs(tt.x); got != tt.want { t.Errorf("Abs(%f) = %v, want %v", tt.x, got, tt.want) } }) } }
為了便于與之前編寫的測試函數(shù) TestAbs
區(qū)分,我為當(dāng)前測試函數(shù)命名為 TestAbs_TableDriven
,代表這是一個(gè)表格驅(qū)動的測試。
在測試函數(shù)內(nèi)部,首先定義了一個(gè)匿名結(jié)構(gòu)體切片,用來保存多個(gè)測試用例。
name
是一個(gè)字符串,可以是任何句子,用來標(biāo)記當(dāng)前測試用例所測試的場景,這樣代碼維護(hù)者通過 name
字段就能夠知道當(dāng)前用例所測試的場景,作用相當(dāng)于代碼注釋。
x
作為 Abs
函數(shù)的入?yún)?,其類型等同?Abs
函數(shù)的參數(shù),如果被測試函數(shù)有多個(gè)參數(shù),這里也可以使用一個(gè)結(jié)構(gòu)體來保存。
want
記錄當(dāng)前測試用例的期望值。
在 for
循環(huán)中,我們可以使用 *testing.T
提供的 t.Run
方法執(zhí)行測試用例,這和直接編寫的 TestXxx
測試函數(shù)沒什么本質(zhì)區(qū)別。
現(xiàn)在使用 go test
命令執(zhí)行測試代碼:
go test -v === RUN TestAbs --- PASS: TestAbs (0.00s) === RUN TestAbs_TableDriven === RUN TestAbs_TableDriven/positive === RUN TestAbs_TableDriven/negative --- PASS: TestAbs_TableDriven (0.00s) --- PASS: TestAbs_TableDriven/positive (0.00s) --- PASS: TestAbs_TableDriven/negative (0.00s) PASS ok github.com/jianghushinian/blog-go-example/test/getting-started/abs 0.145s
可以發(fā)現(xiàn),表格測試的輸出信息更加豐富,能夠分別打印出表格中的每一個(gè)測試用例,并且使用縮進(jìn)來展示層級關(guān)系。
現(xiàn)在我們故意將其中的一個(gè)測試用例改錯(cuò):
{ name: "negative", x: -3, want: 33, }
再次使用 go test
命令執(zhí)行測試代碼看下如何輸出:
go test -v === RUN TestAbs --- PASS: TestAbs (0.00s) === RUN TestAbs_TableDriven === RUN TestAbs_TableDriven/positive === RUN TestAbs_TableDriven/negative abs_test.go:36: Abs(-3.000000) = 3, want 33 --- FAIL: TestAbs_TableDriven (0.00s) --- PASS: TestAbs_TableDriven/positive (0.00s) --- FAIL: TestAbs_TableDriven/negative (0.00s) FAIL exit status 1 FAIL github.com/jianghushinian/blog-go-example/test/getting-started/abs 0.515s
根據(jù)打印結(jié)果,我們很容易能夠發(fā)現(xiàn)是 TestAbs_TableDriven
測試函數(shù)中 negative
這個(gè)測試用例執(zhí)行失敗了。
有些場景下,我們可能想要跳過某些測試用例,可以使用 (*testing.T).Skip
方法來實(shí)現(xiàn):
func TestAbs_Skip(t *testing.T) { // CI 環(huán)境跳過當(dāng)前測試 if os.Getenv("CI") != "" { t.Skip("it's too slow, skip when running in CI") } t.Log(t.Skipped()) got := Abs(-2) if got != 2 { t.Errorf("Abs(-2) = %f; want 2", got) } }
假如 TestAbs_Skip
是一個(gè)非常耗時(shí)的測試用例,我們就可以使用 t.Skip
在 CI 環(huán)境下跳過此測試。
t.Skipped()
返回當(dāng)前測試用例是否被跳過。
使用 go test
命令執(zhí)行測試:
$ CI=1 go test -v -run="TestAbs_Skip" === RUN TestAbs_Skip abs_test.go:46: it's too slow, skip when running in CI --- SKIP: TestAbs_Skip (0.00s) PASS ok github.com/jianghushinian/blog-go-example/test/getting-started/abs 0.103s
這次我們使用 -run
參數(shù)來指定想要執(zhí)行的測試用例,-run
參數(shù)的值支持正則。
并且指定了環(huán)境變量 CI=1
。
從打印結(jié)果來看,TestAbs_Skip
測試用例的確被跳過了,所以 t.Log(t.Skipped())
沒有被執(zhí)行到。
默認(rèn)情況下,測試用例是從上到下按照順序執(zhí)行的,不過,我們可以使用 (*testint.T).Parallel
來標(biāo)記一個(gè)測試函數(shù)支持并發(fā)執(zhí)行:
func TestAbs_Parallel(t *testing.T) { t.Log("Parallel before") // 標(biāo)記當(dāng)前測試支持并行 t.Parallel() t.Log("Parallel after") got := Abs(2) if got != 2 { t.Errorf("Abs(2) = %f; want 2", got) } }
只有一個(gè)測試函數(shù)支持并發(fā)執(zhí)行意義不大,我們可以將 TestAbs
測試函數(shù)也修改為支持并發(fā)執(zhí)行:
func TestAbs(t *testing.T) { t.Parallel() got := Abs(-1) if got != 1 { t.Errorf("Abs(-1) = %f; want 1", got) } }
現(xiàn)在,使用 go test
命令來測試下并發(fā)執(zhí)行測試用例:
$ go test -v -run=".*Parallel.*|^TestAbs$" === RUN TestAbs === PAUSE TestAbs === RUN TestAbs_Parallel abs_test.go:59: Parallel before === PAUSE TestAbs_Parallel === CONT TestAbs --- PASS: TestAbs (0.00s) === CONT TestAbs_Parallel abs_test.go:62: Parallel after --- PASS: TestAbs_Parallel (0.00s) PASS ok github.com/jianghushinian/blog-go-example/test/getting-started/abs 0.200s
這里我們只執(zhí)行了 TestAbs_Parallel
、TestAbs
這兩個(gè)測試函數(shù)。
可以發(fā)現(xiàn),兩個(gè)函數(shù)都不是一次性執(zhí)行完成的,日志中 PAUSE
表示暫停當(dāng)前函數(shù)的執(zhí)行,CONT
表示恢復(fù)當(dāng)前函數(shù)執(zhí)行。
有時(shí)候,我們測試的并不是一個(gè)函數(shù),而是一個(gè)方法,比如我們想要測試 Animal
結(jié)構(gòu)體的 shout
方法:
package animal type Animal struct { Name string } func (a Animal) shout() string { if a.Name == "dog" { return "旺!" } if a.Name == "cat" { return "喵~" } return "吼~" }
那么,測試函數(shù)可以命名為 TestAnimal_shout
,如下是我們針對 Dog
和 Cat
兩種不同的 Animal
對象編寫的測試代碼:
package animal import ( "testing" ) func TestAnimalDog_shout(t *testing.T) { dog := Animal{Name: "dog"} got := dog.shout() want := "旺!" if got != want { t.Errorf("got %s; want %s", got, want) } } func TestAnimalCat_shout(t *testing.T) { cat := Animal{Name: "cat"} got := cat.shout() want := "喵~" if got != want { t.Errorf("got %s; want %s", got, want) } }
黑盒測試
講完了白盒測試,我們再來演示下如何編寫黑盒測試。
要為 Abs
編寫黑盒測試非常簡單,我們只需要將 TestAbs
移動到新的包中即可。
package abs_test import ( "testing" "github.com/jianghushinian/blog-go-example/test/getting-started/abs" ) func TestAbs(t *testing.T) { got := abs.Abs(-1) if got != 1 { t.Errorf("Abs(-1) = %f; want 1", got) } }
因?yàn)楹诤袦y試的函數(shù) TestAbs
與 Abs
不在同一個(gè)包中,所以需要先使用 import
導(dǎo)入 abs
包,之后才能使用 abs.Abs
函數(shù)。
至此,常見的單元測試場景我們就介紹完了。
接下來,我們一起來看如何編寫基準(zhǔn)測試。
基準(zhǔn)測試
基準(zhǔn)測試也叫性能測試,顧名思義,是為了度量程序的性能。
為 Abs
編寫的基準(zhǔn)測試代碼如下:
func BenchmarkAbs(b *testing.B) { for i := 0; i < b.N; i++ { Abs(-1) } }
基準(zhǔn)測試同樣放在 abs_test.go
文件中,以 Benchmark
開頭,參數(shù)不再是 *testing.T
,而是 *testing.B
,在測試函數(shù)中,我們循環(huán)了 b.N
次調(diào)用 Abs(-1)
,b.N
的值是一個(gè)動態(tài)值,我們無需操心,testing
框架會為其分配合理的值,以使測試函數(shù)運(yùn)行足夠多的次數(shù),可以準(zhǔn)確的計(jì)時(shí)。
默認(rèn)情況下,go test
命令并不會運(yùn)行基準(zhǔn)測試,需要指定 -bench
參數(shù):
$ go test -bench="." goos: darwin goarch: arm64 pkg: github.com/jianghushinian/blog-go-example/test/getting-started/abs BenchmarkAbs-8 1000000000 0.5096 ns/op PASS ok github.com/jianghushinian/blog-go-example/test/getting-started/abs 0.674s
-bench
參數(shù)同樣接收一個(gè)正則,.
匹配所有基準(zhǔn)測試。
我們需要重點(diǎn)關(guān)注的是這行結(jié)果:
BenchmarkAbs-8 1000000000 0.5096 ns/op
BenchmarkAbs-8
中,BenchmarkAbs
是測試函數(shù)名,8
是 GOMAXPROCS
的值,即參與執(zhí)行的 CPU 核心數(shù)。
1000000000
表示測試執(zhí)行了這么多次。
0.5096 ns/op
表示每次循環(huán)平均消耗的納秒數(shù)。
如果還想查看基準(zhǔn)測試的內(nèi)存統(tǒng)計(jì)情況,則可以指定 -benchmem
參數(shù):
$ go test -bench="BenchmarkAbs$" -benchmem goos: darwin goarch: arm64 pkg: github.com/jianghushinian/blog-go-example/test/getting-started/abs BenchmarkAbs-8 1000000000 0.5097 ns/op 0 B/op 0 allocs/op PASS ok github.com/jianghushinian/blog-go-example/test/getting-started/abs 0.681s
現(xiàn)在,BenchmarkAbs-8
這行得到了更多輸出:
BenchmarkAbs-8 1000000000 0.5097 ns/op 0 B/op 0 allocs/op
0 B/op
表示每次執(zhí)行測試代碼分配了多少字節(jié)內(nèi)存。
0 allocs/op
表示每次執(zhí)行測試代碼分配了多少次內(nèi)存。
此外,在執(zhí)行 go test
命令時(shí),我們可以使用 -benchtime=Ns
參數(shù)指定基準(zhǔn)測試函數(shù)執(zhí)行時(shí)間為 N
秒:
$ go test -bench="BenchmarkAbs$" -benchtime=0.1s goos: darwin goarch: arm64 pkg: github.com/jianghushinian/blog-go-example/test/getting-started/abs BenchmarkAbs-8 210435709 0.5096 ns/op PASS ok github.com/jianghushinian/blog-go-example/test/getting-started/abs 0.600s
-benchtime
參數(shù)值為 time.Duration
類型支持的時(shí)間格式。
-benchtime
參數(shù)還有一個(gè)特殊語法 -benchtime=Nx
參數(shù),可以指定基準(zhǔn)測試函數(shù)執(zhí)行次數(shù)為 N
次:
$ go test -bench="BenchmarkAbs$" -benchtime=10x goos: darwin goarch: arm64 pkg: github.com/jianghushinian/blog-go-example/test/getting-started/abs BenchmarkAbs-8 10 20.90 ns/op PASS ok github.com/jianghushinian/blog-go-example/test/getting-started/abs 0.391s
有時(shí)候,我們在編寫基準(zhǔn)測試時(shí),被測函數(shù)可能需要一些準(zhǔn)備數(shù)據(jù),而這些準(zhǔn)備數(shù)據(jù)的時(shí)間不應(yīng)該算做被測試函數(shù)的耗時(shí)。
此時(shí),可以使用 (*testing.B).ResetTimer
重置計(jì)時(shí):
func BenchmarkAbsResetTimer(b *testing.B) { time.Sleep(100 * time.Millisecond) // 模擬耗時(shí)的準(zhǔn)備工作 b.ResetTimer() for i := 0; i < b.N; i++ { Abs(-1) } }
這樣,在調(diào)用 b.ResetTimer()
之前的耗時(shí)操作將不被記入測試結(jié)果的耗時(shí)中。
還有一種方法,也可以跳過準(zhǔn)備工作的計(jì)時(shí),即先使用 (*testing.B).StopTimer
停止計(jì)時(shí),耗時(shí)的準(zhǔn)備工作完成后再使用 (*testing.B).StartTimer
恢復(fù)計(jì)時(shí):
func BenchmarkAbsStopTimerStartTimer(b *testing.B) { b.StopTimer() time.Sleep(100 * time.Millisecond) // 模擬耗時(shí)的準(zhǔn)備工作 b.StartTimer() for i := 0; i < b.N; i++ { Abs(-1) } }
默認(rèn)情況下,基準(zhǔn)測試 for
循環(huán)中的代碼是串行執(zhí)行的,如果想要并行執(zhí)行,可以將被測試代碼的調(diào)用放在 (*testing.B).RunParallel
中:
func BenchmarkAbsParallel(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { Abs(-1) } }) }
我們還可以使用 (*testing.B).SetParallelism
控制并發(fā)協(xié)程數(shù):
func BenchmarkAbsParallel(b *testing.B) { b.SetParallelism(2) // 設(shè)置并發(fā) Goroutines 數(shù)量為 2 * GOMAXPROCS b.RunParallel(func(pb *testing.PB) { for pb.Next() { Abs(-1) } }) }
在使用 go test
命令執(zhí)行基準(zhǔn)測試時(shí),可以指定 -cpu
參數(shù)來設(shè)置 GOMAXPROCS
。
要想了解 go test
支持的更多參數(shù),可以使用 go help testflag
命令進(jìn)行查看。
示例測試
示例測試以 Example
開頭,無參數(shù)和返回值,通常存放在 example_test.go
文件中。
約定一個(gè)包、函數(shù) F、類型 T、方法 M 的示例測試命名如下:
func Example() { ... } // 整個(gè)包的示例測試 func ExampleF() { ... } // 函數(shù) F 的示例測試 func ExampleT() { ... } // 類型 T 的示例測試 func ExampleT_M() { ... } // 類型 T 的 M 方法的示例測試
一個(gè)包、函數(shù)、類型、方法如果存在多個(gè)示例測試,可以通過在名稱后面附加一個(gè)不同的后綴來命名示例測試函數(shù),后綴必須以小寫字母開頭,如下:
func Example_suffix() { ... } func ExampleF_suffix() { ... } func ExampleT_suffix() { ... } func ExampleT_M_suffix() { ... }
以下是一個(gè)為 Abs
函數(shù)編寫的示例測試:
func ExampleAbs() { fmt.Println(Abs(-1)) fmt.Println(Abs(2)) // Output: // 1 // 2 }
示例測試函數(shù)末尾需要使用 // Output:
注釋,來標(biāo)記被測試函數(shù)的標(biāo)準(zhǔn)輸出內(nèi)容。
這里分別使用 fmt.Println(Abs(-1))
、fmt.Println(Abs(2))
調(diào)用了兩次 Abs
函數(shù),所以會得到兩個(gè)輸出。
示例測試會攔截測試過程中的標(biāo)準(zhǔn)輸出,并與 // Output:
注釋之后的內(nèi)容做對比,如果相等,則測試通過。
go test
默認(rèn)情況下,不會執(zhí)行示例測試,可以通過 -run
指定示例測試函數(shù):
$ go test -v -run "ExampleAbs$" === RUN ExampleAbs --- PASS: ExampleAbs (0.00s) PASS ok github.com/jianghushinian/blog-go-example/test/getting-started/abs 2.050s
我們還可以使用 // Unordered Output:
注釋,來標(biāo)記被測試函數(shù)的標(biāo)準(zhǔn)輸出內(nèi)容。
這將忽略被測試函數(shù)的輸出順序:
func ExampleAbs_unordered() { fmt.Println(Abs(2)) fmt.Println(Abs(-1)) // Unordered Output: // 1 // 2 }
以上這個(gè)示例測試函數(shù)中,無論是先調(diào)用 Abs(2)
還是先調(diào)用 Abs(-1)
,測試函數(shù)都能通過。
沒有輸出注釋 // Output:
或 // Unordered Output:
的示例測試函數(shù)只會被編譯,但不會執(zhí)行。
此外,示例測試還有一個(gè)非常有用功能,它能被 godoc
或 pkgsite
工具所識別,將示例函數(shù)的代碼提取后作為被測試函數(shù)文檔的一部分。
注意:舊版本的 Go 自帶了
godoc
工具,能夠在本地啟動一個(gè) Web 服務(wù)器,對本地安裝的 Go 包提供文檔服務(wù),不過現(xiàn)在官方已經(jīng)不維護(hù)godoc
了,所以不再推薦使用。Go 1.15 以后雖然集成了go doc
工具,但是無法啟動 Web 服務(wù),比較適合命令行中查看 Go 包的文檔?,F(xiàn)在,Go 官方比較推薦使用的工具是pkgsite
,能夠啟動 Web 服務(wù),并且它與 Go 在線文檔站點(diǎn)長得一樣。
這里以 pkgsite
工具為例展示下示例測試函數(shù)生成的文檔效果。
首先,安裝 pkgsite
:
$ go install golang.org/x/pkgsite/cmd/pkgsite@latest
然后,在 abs.go
目錄下執(zhí)行 pkgsite
即可啟動文檔服務(wù):
$ pkgsite
現(xiàn)在訪問 http://localhost:8080
即可進(jìn)入文檔服務(wù)首頁:
點(diǎn)擊模塊名 github.com/jianghushinian/blog-go-example/test/getting-started
即可找到 Abs
函數(shù)位置,在 Abs
函數(shù)下方,標(biāo)題 Example
、Example (Unordered)
下就是通過示例測試生成的示例文檔:
注意:這里需要額外提及的一點(diǎn)是,我們查看文檔的本地包模塊名稱(module
)應(yīng)該帶 .
,也就是一般使用域名作為包名的一部分,否則啟動 pkgsite
后將會報(bào)錯(cuò),無法查看本地包的文檔。
模糊測試
模糊測試在 Go 1.18 中被引入,模糊測試(fuzz testing
)又叫隨機(jī)測試,是一種基于隨機(jī)輸入的自動化測試技術(shù)。
模糊測試比較適合用于發(fā)現(xiàn)處理用戶輸入的代碼中存在的問題。
關(guān)于模糊測試的編寫方式,有一張圖廣泛流傳:
模糊測試同樣需要放在 _test.go
文件中,并且以 Fuzz
開頭,參數(shù)為 *testing.F
。
上圖中,f.Add(5, "hello")
是在為模糊測試提供初始的種子語料,其實(shí)就是被測試函數(shù)接收的合法參數(shù),后續(xù)的模糊測試過程中,會根據(jù)這個(gè)種子語料,生成更多的模糊測試參數(shù)。這有點(diǎn)類似我們生成隨機(jī)數(shù)時(shí)需要傳遞一個(gè)隨機(jī)種子。雖然調(diào)用 f.Add
方法不是必須的,但提供合法的種子語料有利于更早發(fā)現(xiàn)被測試函數(shù)的問題。
f.Fuzz
是模糊測試的主體邏輯,它接收一個(gè)函數(shù),函數(shù)的第一個(gè)參數(shù)為 *testing.T
,之后是被測函數(shù)接收的參數(shù),稱為 Fuzzing arguments
。
Fuzzing arguments
參數(shù)是 testing
框架隨機(jī)生成的,所以叫隨機(jī)測試,這些隨機(jī)生成的參數(shù)將依次傳遞給 Foo
函數(shù)。
調(diào)用 Foo
函數(shù)和判斷測試結(jié)果是否正確的代碼,就跟我們編寫的普通單元測試一樣了。
可以發(fā)現(xiàn),模糊測試相較于單元測試,多了一個(gè)自動生成測試參數(shù)的過程。
不過,Fuzzing arguments
支持的參數(shù)類型有限,僅支持如下幾種類型:
- string, []byte
- int, int8, int16, int32/rune, int64
- uint, uint8/byte, uint16, uint32, uint64
- float32, float64
- bool
此外,編寫模糊測試時(shí),Fuzz target
不要依賴全局狀態(tài),因?yàn)槟:郎y試會并行執(zhí)行。
為了演示如何編寫模糊測試,我編寫了一個(gè) Hello
函數(shù):
package hello import "errors" var ( ErrEmptyName = errors.New("empty name") ErrTooLongName = errors.New("too long name") ) func Hello(name string) (string, error) { if name == "" { return "", ErrEmptyName } if len(name) > 10 { return "", ErrTooLongName } return "Hello " + name, nil }
將 Hello
函數(shù)放在 hello.go
文件中。
在 Hello
函數(shù)內(nèi)部,對 name
參數(shù)進(jìn)行了校驗(yàn),不能為空,且長度不能超過 10。
為 Hello
函數(shù)編寫模糊測試代碼如下:
func FuzzHello(f *testing.F) { f.Add("Foo") f.Fuzz(func(t *testing.T, name string) { _, err := Hello(name) if err != nil { if errors.Is(err, ErrEmptyName) || errors.Is(err, ErrTooLongName) { return } t.Errorf("unexpected error: %s, name: %s", err, name) } }) }
模糊測試代碼放在 hello_fuzz_test.go
中,包名同樣為 hello
。
在 f.Fuzz
中調(diào)用了 Hello
函數(shù),并判斷返回的 err
是否符合預(yù)期,如果不符合預(yù)期,則表示測試失敗。
go test
命令默認(rèn)情況下同樣不會執(zhí)行模糊測試,我們需要指定 -fuzz
參數(shù):
$ go test -fuzz="FuzzHello" fuzz: elapsed: 0s, gathering baseline coverage: 0/1 completed fuzz: elapsed: 0s, gathering baseline coverage: 1/1 completed, now fuzzing with 8 workers fuzz: elapsed: 3s, execs: 521335 (173703/sec), new interesting: 2 (total: 3) fuzz: elapsed: 6s, execs: 947014 (141945/sec), new interesting: 2 (total: 3) fuzz: elapsed: 9s, execs: 1391822 (148228/sec), new interesting: 2 (total: 3) fuzz: elapsed: 12s, execs: 1838008 (148764/sec), new interesting: 2 (total: 3) fuzz: elapsed: 15s, execs: 2266978 (143002/sec), new interesting: 2 (total: 3) ^Cfuzz: elapsed: 15s, execs: 2308214 (139431/sec), new interesting: 2 (total: 3) PASS ok github.com/jianghushinian/blog-go-example/test/getting-started/hello 17.131s
以上測試執(zhí)行過程中,我使用 ^C
終止了測試。模糊測試默認(rèn)情況下會一直執(zhí)行下去,直至遇到 crash 終止。
通過以上示例,我們可以發(fā)現(xiàn),模糊測試之所以強(qiáng)大,就是因?yàn)槠鋾恢眻?zhí)行,不斷生成測試參數(shù),以覆蓋更多的情況和邊界條件。
也正因?yàn)槿绱?,模糊測試通常不建議在 CI 中執(zhí)行。
不過,我們可以使用 -fuzztime
限制模糊測試執(zhí)行的時(shí)間:
go test -fuzz="FuzzHello" -fuzztime 10s fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers fuzz: elapsed: 3s, execs: 470505 (156828/sec), new interesting: 0 (total: 3) fuzz: elapsed: 6s, execs: 948821 (159392/sec), new interesting: 0 (total: 3) fuzz: elapsed: 9s, execs: 1423720 (158326/sec), new interesting: 0 (total: 3) fuzz: elapsed: 10s, execs: 1573524 (139348/sec), new interesting: 0 (total: 3) PASS ok github.com/jianghushinian/blog-go-example/test/getting-started/hello 11.820s
這次,我沒有按 ^C
鍵終止測試,而是 10 秒過后,模糊測試自動終止。
現(xiàn)在,我們修改下 Hello
函數(shù),使其返回一個(gè)未知的錯(cuò)誤:
func Hello(name string) (string, error) { if name == "" { return "", ErrEmptyName } if len(name) > 10 { return "", ErrTooLongName } if name == "Bob" { return "", errors.New("not allowed") } return "Hello " + name, nil }
當(dāng) name
值為 Bob
時(shí),Hello
函數(shù)將返回一個(gè)未知錯(cuò)誤,模擬 BUG 場景。
再次使用 go test
命令執(zhí)行模糊測試:
$ go test -fuzz="FuzzHello" fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers fuzz: minimizing 30-byte failing input file fuzz: elapsed: 1s, minimizing --- FAIL: FuzzHello (1.17s) --- FAIL: FuzzHello (0.00s) hello_fuzz_test.go:18: unexpected error: not allowed, name: Bob Failing input written to testdata/fuzz/FuzzHello/19f92ff5a07664a0 To re-run: go test -run=FuzzHello/19f92ff5a07664a0 FAIL exit status 1 FAIL github.com/jianghushinian/blog-go-example/test/getting-started/hello 1.626s
可以發(fā)現(xiàn),這次模糊測試失敗了。
根據(jù)測試結(jié)果,是在執(zhí)行 FuzzHello/19f92ff5a07664a0
時(shí)失敗的,19f92ff5a07664a0
是模糊測試生成的文件,位于 testdata/fuzz/FuzzHello/19f92ff5a07664a0
。
使用 tree
命令查看 19f92ff5a07664a0
位置:
tree hello hello ├── hello.go ├── hello_fuzz_test.go └── testdata └── fuzz └── FuzzHello └── 19f92ff5a07664a0
testdata
目錄及目錄下所有內(nèi)容都是模糊測試自動生成的。
19f92ff5a07664a0
文件內(nèi)容如下:
go test fuzz v1 string("Bob")
文件第一行 go test fuzz v1
是模糊測試要求的文件頭,用于標(biāo)識這是一個(gè)種子語料文件,并且使用的編解碼器的版本為 v1
。
第二行就是種子語料,是一個(gè) Go 代碼片段,即 string
類型的 Bob
參數(shù)。正是這個(gè)參數(shù),引發(fā)了錯(cuò)誤。
至此,我們使用模糊測試發(fā)現(xiàn)了 Hello
函數(shù)中隱藏的 BUG,這在黑盒測試中尤其有效,我們無需查看 Hello
函數(shù)內(nèi)部代碼,為每個(gè)邊界條件編寫測試用例,模糊測試會自動生成大量的隨機(jī)參數(shù),檢測程序的異常。
測試覆蓋率
go test
命令支持使用 -cover
標(biāo)志查看測試覆蓋率:
$ go test -cover ./... ? github.com/jianghushinian/blog-go-example/test/getting-started [no test files] ok github.com/jianghushinian/blog-go-example/test/getting-started/abs 2.334s coverage: 100.0% of statements ok github.com/jianghushinian/blog-go-example/test/getting-started/animal 1.924s coverage: 100.0% of statements ok github.com/jianghushinian/blog-go-example/test/getting-started/hello 2.738s coverage: 60.0% of statements ok github.com/jianghushinian/blog-go-example/test/getting-started/test/abs 3.136s coverage: [no statements]
注意:根據(jù)我的實(shí)際測試結(jié)果來看,測試覆蓋率默認(rèn)僅包含單元測試、示例測試和模糊測試(模糊測試僅執(zhí)行
f.Add
添加的種子參數(shù)測試),基準(zhǔn)測試并不會被統(tǒng)計(jì)。要想將基準(zhǔn)測試納入覆蓋率統(tǒng)計(jì),需要增加-bench
參數(shù)。你可以增加-v
函數(shù)查看更詳細(xì)信息。
此外,go test
命令還支持使用 -coverprofile
參數(shù)生成覆蓋率 profile
文件:
$ go test -coverprofile=coverage.out ./... ? github.com/jianghushinian/blog-go-example/test/getting-started [no test files] ok github.com/jianghushinian/blog-go-example/test/getting-started/abs 3.101s coverage: 100.0% of statements ok github.com/jianghushinian/blog-go-example/test/getting-started/animal 3.495s coverage: 100.0% of statements ok github.com/jianghushinian/blog-go-example/test/getting-started/hello 3.910s coverage: 60.0% of statements ok github.com/jianghushinian/blog-go-example/test/getting-started/test/abs 4.312s coverage: [no statements]
命令執(zhí)行后,將在當(dāng)前目錄生成一個(gè) coverage.out
文件,內(nèi)容如下:
cat coverage.out mode: set github.com/jianghushinian/blog-go-example/test/getting-started/abs/abs.go:5.29,7.2 1 1 github.com/jianghushinian/blog-go-example/test/getting-started/animal/animal.go:7.32,8.21 1 1 github.com/jianghushinian/blog-go-example/test/getting-started/animal/animal.go:8.21,10.3 1 1 github.com/jianghushinian/blog-go-example/test/getting-started/animal/animal.go:11.2,11.21 1 1 github.com/jianghushinian/blog-go-example/test/getting-started/animal/animal.go:11.21,13.3 1 1 github.com/jianghushinian/blog-go-example/test/getting-started/animal/animal.go:14.2,14.17 1 1 github.com/jianghushinian/blog-go-example/test/getting-started/hello/hello.go:10.41,11.16 1 1 github.com/jianghushinian/blog-go-example/test/getting-started/hello/hello.go:11.16,13.3 1 0 github.com/jianghushinian/blog-go-example/test/getting-started/hello/hello.go:14.2,14.20 1 1 github.com/jianghushinian/blog-go-example/test/getting-started/hello/hello.go:14.20,16.3 1 0 github.com/jianghushinian/blog-go-example/test/getting-started/hello/hello.go:17.2,17.29 1 1
有了 coverage.out
文件,我們可以直接使用 go tool cover
命令查看測試覆蓋率:
$ go tool cover -func=coverage.out github.com/jianghushinian/blog-go-example/test/getting-started/abs/abs.go:5: Abs 100.0% github.com/jianghushinian/blog-go-example/test/getting-started/animal/animal.go:7: shout 100.0% github.com/jianghushinian/blog-go-example/test/getting-started/hello/hello.go:10: Hello 60.0% total: (statements) 81.8%
以上方式,實(shí)現(xiàn)了在命令行查看程序測試覆蓋率。
我們還可以通過 go tool cover
命令以可視化的方式查看測試覆蓋率:
$ go tool cover -html=coverage.out -o=coverage.html
執(zhí)行命令后,會在當(dāng)前目錄下生成 coverage.html
文件,使用瀏覽器打開內(nèi)容如下:
在頁面頂部左側(cè),可以切換查看不同的測試文件和對應(yīng)測試覆蓋率。
灰色代碼表示未被跟蹤 not tracked
。
紅色部分表示未被測試的代碼 not covered
。
綠色部分表示已經(jīng)被測試覆蓋的代碼 covered
。
這樣,我們就可以更加直觀的查看和分析代碼測試覆蓋率了。
總結(jié)
本文向大家介紹了 Go 中編寫各種測試代碼的方式。
Go 支持單元測試、基準(zhǔn)測試、示例測試以及模糊測試四種測試方法。
單元測試是我們最常使用的測試方法,如果被測代碼需要編寫多個(gè)測試用例,可以使用表格測試。
基準(zhǔn)測試能夠測量程序的性能指標(biāo),默認(rèn)情況下 go test
不會執(zhí)行基準(zhǔn)測試,需要指定 -bench regexp
參數(shù)才可以執(zhí)行。
示例測試可以測試程序的標(biāo)準(zhǔn)輸出內(nèi)容,并且能夠配合 pkgsite
工具,在查看本地包文檔時(shí)作為被測函數(shù)文檔的一部分。
模糊測試是 Go 1.18 版本引入的,是一種基于隨機(jī)輸入的自動化測試技術(shù),非常強(qiáng)大,適合用于發(fā)現(xiàn)處理用戶輸入的代碼中存在的問題。
根據(jù)測試代碼與被測代碼是否在同一個(gè)包中,測試又可以分為白盒測試和黑盒測試,我們應(yīng)該盡量編寫白盒測試。
可以使用 go test -cover
查看測試覆蓋率,我們可以將測試覆蓋率基線集成到 CI 中,來保證單元測試覆蓋率。
go test
命令支持的更多參數(shù)可以通過 go help testflag
命令查看。
由于篇幅所限,本文僅算做是 Go 單元測試的基礎(chǔ)入門,更多單元測試在實(shí)戰(zhàn)場景中的應(yīng)用,我會在后續(xù)文章中進(jìn)行講解,敬請期待。
本文完整代碼示例:blog-go-example/test/getting-started at main · jianghushinian/blog-go-example · GitHub
以上就是在Go中編寫測試代碼的方法總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于Go編寫測試代碼的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go操作各大消息隊(duì)列教程(RabbitMQ、Kafka)
消息隊(duì)列是一種異步的服務(wù)間通信方式,適用于無服務(wù)器和微服務(wù)架構(gòu),本文主要介紹了Go操作各大消息隊(duì)列教程(RabbitMQ、Kafka),需要的朋友可以了解一下2024-02-02Go讀取yaml文件到struct類的實(shí)現(xiàn)方法
本文主要介紹了Go讀取yaml文件到struct類,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01go責(zé)任鏈行為型設(shè)計(jì)模式Chain?Of?Responsibility
這篇文章主要為大家介紹了go行為型設(shè)計(jì)模式之責(zé)任鏈Chain?Of?Responsibility使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12go Antlr重構(gòu)腳本解釋器實(shí)現(xiàn)示例
這篇文章主要為大家介紹了go Antlr重構(gòu)腳本解釋器實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08golang 并發(fā)編程之生產(chǎn)者消費(fèi)者詳解
這篇文章主要介紹了golang 并發(fā)編程之生產(chǎn)者消費(fèi)者詳解,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-05-05Go調(diào)度器學(xué)習(xí)之goroutine調(diào)度詳解
這篇文章主要為大家詳細(xì)介紹了Go調(diào)度器中g(shù)oroutine調(diào)度的相關(guān)知識,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-03-03