在Go中編寫測試代碼的方法總結(jié)
測試分類
在 Go 中,編寫的測試用例可以分為四類:
單元測試:測試函數(shù)名稱以
Test開頭,如TestXxx、Test_Xxx,用來對程序的最小單元進行測試,如函數(shù)、方法等。基準(zhǔn)測試:也叫性能測試,測試函數(shù)名稱以
Benchmark開頭,用來測量程序的性能指標(biāo)。示例測試:測試函數(shù)名稱以
Example開頭,可以用來測試程序的標(biāo)準(zhǔn)輸出內(nèi)容。模糊測試:也叫隨機測試,測試函數(shù)名稱以
Fuzz開頭,是一種基于隨機輸入的自動化測試技術(shù),適合用來測試處理用戶輸入的代碼,在 Go 1.18 中被引入。
這四類測試用例都有各自的適用場景,其中單元測試最為常用,你一定要掌握。
以上這些測試用例都可以使用 go test 命令來執(zhí)行。
測試規(guī)范
編寫測試代碼并不需要我們學(xué)習(xí)新的 Go 語法,但有些測試規(guī)范還是需要遵守的。
Go 在提供 testing 測試框架時,就規(guī)定了很多測試規(guī)范,用來約束我們編寫測試的方式,這有助于項目的工程化。
測試文件命名規(guī)范
首先,測試文件命名必須以 _test.go 結(jié)尾,否則將被測試框架忽略。比如我們的 Go 代碼文件名為 hello.go,則測試文件可以命名為 hello_test.go。
只有以 _test.go 結(jié)尾的測試文件,才能使用 go test 命令執(zhí)行。
在構(gòu)建 Go 程序時,go build 命令會忽略以 _test.go 結(jié)尾的測試文件。
測試包命名規(guī)范
測試用例除了根據(jù)使用場景可以分為四類,還可以根據(jù)代碼和測試用例是否在一個包中,分為白盒測試和黑盒測試。
白盒測試:將測試代碼和被測代碼放在同一個包中,也就是二者包名相同,這些測試用例屬于白盒測試。比如 Go 代碼文件名為
hello.go,包名為hello,測試文件可以命名為hello_test.go,并且必須與hello.go放在同一個目錄下,包名也必須為hello。白盒測試的測試用例可以使用和測試當(dāng)前包中所有標(biāo)識符(變量、函數(shù)等),包括未導(dǎo)出的標(biāo)識符。黑盒測試:將測試代碼和被測代碼放在不同的包中,即包名不同,這些測試用例屬于黑盒測試。比如 Go 代碼文件名為
hello.go,包名為hello,測試文件同樣可以命名為hello_test.go,與hello.go放在同一個目錄下,但包名不能再叫hello,應(yīng)該命名為hello_test,hello_test.go文件也可以放在專門的test目錄下,此時可以隨意命名包名。黑盒測試的測試用例僅能夠使用和測試被測代碼包中可導(dǎo)出的標(biāo)識符,因為二者已經(jīng)不再屬于同一個包,這遵循 Go 語法規(guī)范。
根據(jù)二者各自特點,在開發(fā)時我們應(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)測試和模糊測試都接收一個參數(shù),由 testing 框架提供,示例測試則不需要傳遞參數(shù)。
其中 Xxx 一般是被測試的函數(shù)名稱,首字母必須大寫。如果是以 Test_Xxx 方式命名測試函數(shù),則 Xxx 首字母大小寫均可。
測試變量命名規(guī)范
對于測試變量的命名,testing 框架沒有有強制約束,但社區(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ù)的實際輸出和測試函數(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ù)喜好和團隊中的開發(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 代表多個測試用例,循環(huán)時以 tt 作為循環(huán)變量(tt 可以避免與單元測試函數(shù)的參數(shù)變量 t 命名沖突)。
表格測試還有另一個版本:
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 代表多個測試用例,循環(huán)時以 cc 作為循環(huán)變量(cc 可以避免與常見的 context 縮寫 c 命名沖突)。
編寫測試代碼的常見規(guī)范我們就先講解到這里,更多規(guī)范將在下文講解對應(yīng)示例時再進行詳細說明。
單元測試
單元測試是我們最常編寫的測試用例,所以先來學(xué)習(xí)下如何編寫單元測試。
首先,我們準(zhǔn)備一個 Abs 函數(shù)作為被測試的代碼,存放于 abs.go 文件中,其包名為 abs,代碼如下:
package abs
import "math"
func Abs(x float64) float64 {
return math.Abs(x)
}白盒測試
現(xiàn)在為 Abs 編寫一個白盒測試函數(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 打印錯誤信息。
參數(shù) *testing.T 是一個結(jié)構(gòu)體指針,提供了如下幾個方法用于錯誤報告:
t.Log/t.Logf:打印正常日志信息,類似fmt.Print。t.Error/t.Errorf:打印測試失敗時的錯誤信息,不影響當(dāng)前測試函數(shù)內(nèi)后續(xù)代碼的繼續(xù)執(zhí)行。t.Fatal/t.Fatalf:打印測試失敗時的錯誤信息,并終止當(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ù)名錯誤的寫成 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í)行。
通常情況下,我們不會對一個函數(shù)只做一種輸入?yún)?shù)的測試,為了提高測試覆蓋率,我們可能還需要多測試幾種參數(shù)的用例,比如測試下 Abs(2)、Abs(3) 等是否正確。
這時,可以像如下這樣編寫測試函數(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,代表這是一個表格驅(qū)動的測試。
在測試函數(shù)內(nèi)部,首先定義了一個匿名結(jié)構(gòu)體切片,用來保存多個測試用例。
name 是一個字符串,可以是任何句子,用來標(biāo)記當(dāng)前測試用例所測試的場景,這樣代碼維護者通過 name 字段就能夠知道當(dāng)前用例所測試的場景,作用相當(dāng)于代碼注釋。
x 作為 Abs 函數(shù)的入?yún)?,其類型等同?Abs 函數(shù)的參數(shù),如果被測試函數(shù)有多個參數(shù),這里也可以使用一個結(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),表格測試的輸出信息更加豐富,能夠分別打印出表格中的每一個測試用例,并且使用縮進來展示層級關(guān)系。
現(xiàn)在我們故意將其中的一個測試用例改錯:
{
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 這個測試用例執(zhí)行失敗了。
有些場景下,我們可能想要跳過某些測試用例,可以使用 (*testing.T).Skip 方法來實現(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 是一個非常耗時的測試用例,我們就可以使用 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)記一個測試函數(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)
}
}只有一個測試函數(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 這兩個測試函數(shù)。
可以發(fā)現(xiàn),兩個函數(shù)都不是一次性執(zhí)行完成的,日志中 PAUSE 表示暫停當(dāng)前函數(shù)的執(zhí)行,CONT 表示恢復(fù)當(dāng)前函數(shù)執(zhí)行。
有時候,我們測試的并不是一個函數(shù),而是一個方法,比如我們想要測試 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)
}
}因為黑盒測試的函數(shù) TestAbs 與 Abs 不在同一個包中,所以需要先使用 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 的值是一個動態(tài)值,我們無需操心,testing 框架會為其分配合理的值,以使測試函數(shù)運行足夠多的次數(shù),可以準(zhǔn)確的計時。
默認(rèn)情況下,go test 命令并不會運行基準(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ù)同樣接收一個正則,. 匹配所有基準(zhǔ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)計情況,則可以指定 -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 命令時,我們可以使用 -benchtime=Ns 參數(shù)指定基準(zhǔn)測試函數(shù)執(zhí)行時間為 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 類型支持的時間格式。
-benchtime 參數(shù)還有一個特殊語法 -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
有時候,我們在編寫基準(zhǔn)測試時,被測函數(shù)可能需要一些準(zhǔn)備數(shù)據(jù),而這些準(zhǔn)備數(shù)據(jù)的時間不應(yīng)該算做被測試函數(shù)的耗時。
此時,可以使用 (*testing.B).ResetTimer 重置計時:
func BenchmarkAbsResetTimer(b *testing.B) {
time.Sleep(100 * time.Millisecond) // 模擬耗時的準(zhǔn)備工作
b.ResetTimer()
for i := 0; i < b.N; i++ {
Abs(-1)
}
}這樣,在調(diào)用 b.ResetTimer() 之前的耗時操作將不被記入測試結(jié)果的耗時中。
還有一種方法,也可以跳過準(zhǔn)備工作的計時,即先使用 (*testing.B).StopTimer 停止計時,耗時的準(zhǔn)備工作完成后再使用 (*testing.B).StartTimer 恢復(fù)計時:
func BenchmarkAbsStopTimerStartTimer(b *testing.B) {
b.StopTimer()
time.Sleep(100 * time.Millisecond) // 模擬耗時的準(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)測試時,可以指定 -cpu 參數(shù)來設(shè)置 GOMAXPROCS。
要想了解 go test 支持的更多參數(shù),可以使用 go help testflag 命令進行查看。
示例測試
示例測試以 Example 開頭,無參數(shù)和返回值,通常存放在 example_test.go 文件中。
約定一個包、函數(shù) F、類型 T、方法 M 的示例測試命名如下:
func Example() { ... } // 整個包的示例測試
func ExampleF() { ... } // 函數(shù) F 的示例測試
func ExampleT() { ... } // 類型 T 的示例測試
func ExampleT_M() { ... } // 類型 T 的 M 方法的示例測試一個包、函數(shù)、類型、方法如果存在多個示例測試,可以通過在名稱后面附加一個不同的后綴來命名示例測試函數(shù),后綴必須以小寫字母開頭,如下:
func Example_suffix() { ... }
func ExampleF_suffix() { ... }
func ExampleT_suffix() { ... }
func ExampleT_M_suffix() { ... }以下是一個為 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ù),所以會得到兩個輸出。
示例測試會攔截測試過程中的標(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
}以上這個示例測試函數(shù)中,無論是先調(diào)用 Abs(2) 還是先調(diào)用 Abs(-1),測試函數(shù)都能通過。
沒有輸出注釋 // Output: 或 // Unordered Output:的示例測試函數(shù)只會被編譯,但不會執(zhí)行。
此外,示例測試還有一個非常有用功能,它能被 godoc 或 pkgsite 工具所識別,將示例函數(shù)的代碼提取后作為被測試函數(shù)文檔的一部分。
注意:舊版本的 Go 自帶了
godoc工具,能夠在本地啟動一個 Web 服務(wù)器,對本地安裝的 Go 包提供文檔服務(wù),不過現(xiàn)在官方已經(jīng)不維護godoc了,所以不再推薦使用。Go 1.15 以后雖然集成了go doc工具,但是無法啟動 Web 服務(wù),比較適合命令行中查看 Go 包的文檔?,F(xiàn)在,Go 官方比較推薦使用的工具是pkgsite,能夠啟動 Web 服務(wù),并且它與 Go 在線文檔站點長得一樣。
這里以 pkgsite 工具為例展示下示例測試函數(shù)生成的文檔效果。
首先,安裝 pkgsite:
$ go install golang.org/x/pkgsite/cmd/pkgsite@latest
然后,在 abs.go 目錄下執(zhí)行 pkgsite 即可啟動文檔服務(wù):
$ pkgsite
現(xiàn)在訪問 http://localhost:8080 即可進入文檔服務(wù)首頁:

點擊模塊名 github.com/jianghushinian/blog-go-example/test/getting-started 即可找到 Abs 函數(shù)位置,在 Abs 函數(shù)下方,標(biāo)題 Example、Example (Unordered) 下就是通過示例測試生成的示例文檔:

注意:這里需要額外提及的一點是,我們查看文檔的本地包模塊名稱(module)應(yīng)該帶 .,也就是一般使用域名作為包名的一部分,否則啟動 pkgsite 后將會報錯,無法查看本地包的文檔。
模糊測試
模糊測試在 Go 1.18 中被引入,模糊測試(fuzz testing)又叫隨機測試,是一種基于隨機輸入的自動化測試技術(shù)。
模糊測試比較適合用于發(fā)現(xiàn)處理用戶輸入的代碼中存在的問題。
關(guān)于模糊測試的編寫方式,有一張圖廣泛流傳:

模糊測試同樣需要放在 _test.go 文件中,并且以 Fuzz 開頭,參數(shù)為 *testing.F。
上圖中,f.Add(5, "hello") 是在為模糊測試提供初始的種子語料,其實就是被測試函數(shù)接收的合法參數(shù),后續(xù)的模糊測試過程中,會根據(jù)這個種子語料,生成更多的模糊測試參數(shù)。這有點類似我們生成隨機數(shù)時需要傳遞一個隨機種子。雖然調(diào)用 f.Add 方法不是必須的,但提供合法的種子語料有利于更早發(fā)現(xiàn)被測試函數(shù)的問題。
f.Fuzz 是模糊測試的主體邏輯,它接收一個函數(shù),函數(shù)的第一個參數(shù)為 *testing.T,之后是被測函數(shù)接收的參數(shù),稱為 Fuzzing arguments。
Fuzzing arguments 參數(shù)是 testing 框架隨機生成的,所以叫隨機測試,這些隨機生成的參數(shù)將依次傳遞給 Foo 函數(shù)。
調(diào)用 Foo 函數(shù)和判斷測試結(jié)果是否正確的代碼,就跟我們編寫的普通單元測試一樣了。
可以發(fā)現(xiàn),模糊測試相較于單元測試,多了一個自動生成測試參數(shù)的過程。
不過,Fuzzing arguments 支持的參數(shù)類型有限,僅支持如下幾種類型:
- string, []byte
- int, int8, int16, int32/rune, int64
- uint, uint8/byte, uint16, uint32, uint64
- float32, float64
- bool
此外,編寫模糊測試時,Fuzz target 不要依賴全局狀態(tài),因為模糊測試會并行執(zhí)行。
為了演示如何編寫模糊測試,我編寫了一個 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ù)進行了校驗,不能為空,且長度不能超過 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),模糊測試之所以強大,就是因為其會一直執(zhí)行,不斷生成測試參數(shù),以覆蓋更多的情況和邊界條件。
也正因為如此,模糊測試通常不建議在 CI 中執(zhí)行。
不過,我們可以使用 -fuzztime 限制模糊測試執(zhí)行的時間:
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ù),使其返回一個未知的錯誤:
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 時,Hello 函數(shù)將返回一個未知錯誤,模擬 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 時失敗的,19f92ff5a07664a0 是模糊測試生成的文件,位于 testdata/fuzz/FuzzHello/19f92ff5a07664a0。
使用 tree 命令查看 19f92ff5a07664a0 位置:
tree hello
hello
├── hello.go
├── hello_fuzz_test.go
└── testdata
└── fuzz
└── FuzzHello
└── 19f92ff5a07664a0testdata 目錄及目錄下所有內(nèi)容都是模糊測試自動生成的。
19f92ff5a07664a0 文件內(nèi)容如下:
go test fuzz v1
string("Bob")文件第一行 go test fuzz v1 是模糊測試要求的文件頭,用于標(biāo)識這是一個種子語料文件,并且使用的編解碼器的版本為 v1。
第二行就是種子語料,是一個 Go 代碼片段,即 string 類型的 Bob 參數(shù)。正是這個參數(shù),引發(fā)了錯誤。
至此,我們使用模糊測試發(fā)現(xiàn)了 Hello 函數(shù)中隱藏的 BUG,這在黑盒測試中尤其有效,我們無需查看 Hello 函數(shù)內(nèi)部代碼,為每個邊界條件編寫測試用例,模糊測試會自動生成大量的隨機參數(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ù)我的實際測試結(jié)果來看,測試覆蓋率默認(rèn)僅包含單元測試、示例測試和模糊測試(模糊測試僅執(zhí)行
f.Add添加的種子參數(shù)測試),基準(zhǔn)測試并不會被統(tǒng)計。要想將基準(zhǔn)測試納入覆蓋率統(tǒng)計,需要增加-bench參數(shù)。你可以增加-v函數(shù)查看更詳細信息。
此外,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)前目錄生成一個 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%
以上方式,實現(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)測試、示例測試以及模糊測試四種測試方法。
單元測試是我們最常使用的測試方法,如果被測代碼需要編寫多個測試用例,可以使用表格測試。
基準(zhǔn)測試能夠測量程序的性能指標(biāo),默認(rèn)情況下 go test 不會執(zhí)行基準(zhǔn)測試,需要指定 -bench regexp 參數(shù)才可以執(zhí)行。
示例測試可以測試程序的標(biāo)準(zhǔn)輸出內(nèi)容,并且能夠配合 pkgsite 工具,在查看本地包文檔時作為被測函數(shù)文檔的一部分。
模糊測試是 Go 1.18 版本引入的,是一種基于隨機輸入的自動化測試技術(shù),非常強大,適合用于發(fā)現(xiàn)處理用戶輸入的代碼中存在的問題。
根據(jù)測試代碼與被測代碼是否在同一個包中,測試又可以分為白盒測試和黑盒測試,我們應(yīng)該盡量編寫白盒測試。
可以使用 go test -cover 查看測試覆蓋率,我們可以將測試覆蓋率基線集成到 CI 中,來保證單元測試覆蓋率。
go test 命令支持的更多參數(shù)可以通過 go help testflag 命令查看。
由于篇幅所限,本文僅算做是 Go 單元測試的基礎(chǔ)入門,更多單元測試在實戰(zhàn)場景中的應(yīng)用,我會在后續(xù)文章中進行講解,敬請期待。
本文完整代碼示例:blog-go-example/test/getting-started at main · jianghushinian/blog-go-example · GitHub
以上就是在Go中編寫測試代碼的方法總結(jié)的詳細內(nèi)容,更多關(guān)于Go編寫測試代碼的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
用golang實現(xiàn)一個定時器任務(wù)隊列實例
golang中提供了2種定時器timer和ticker,分別是一次性定時器和重復(fù)任務(wù)定時器。這篇文章主要介紹了用golang實現(xiàn)一個定時器任務(wù)隊列實例,非常具有實用價值,需要的朋友可以參考下2018-05-05
淺析go語言如何實現(xiàn)協(xié)程的搶占式調(diào)度的
go語言通過GMP模型實現(xiàn)協(xié)程并發(fā),為了避免單協(xié)程持續(xù)持有線程導(dǎo)致線程隊列中的其他協(xié)程饑餓問題,設(shè)計者提出了一個搶占式調(diào)度機制,本文會基于一個簡單的代碼示例對搶占式調(diào)度過程進行深入講解剖析2024-04-04
Golang實現(xiàn)HTTP代理突破IP訪問限制的步驟詳解
在當(dāng)今互聯(lián)網(wǎng)時代,網(wǎng)站和服務(wù)商為了維護安全性和保護用戶隱私,常常會對特定的IP地址進行封鎖或限制,本文將介紹如何使用Golang實現(xiàn)HTTP代理來突破IP訪問限制,需要的朋友可以參考下2023-10-10
用Go+WebSocket快速實現(xiàn)一個chat服務(wù)
這篇文章主要介紹了用Go+WebSocket快速實現(xiàn)一個chat服務(wù),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04
go-redis Pipeline與事務(wù)的實現(xiàn)示例
本文介紹了Go-Redis v9中Pipeline、事務(wù)和Watch機制的使用方法與優(yōu)化實踐,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-11-11
Go語言使用protojson庫實現(xiàn)Protocol Buffers與JSON轉(zhuǎn)換
本文主要介紹Google開源的工具庫Protojson庫如何Protocol Buffers與JSON進行轉(zhuǎn)換,以及和標(biāo)準(zhǔn)庫encoding/json的性能對比,需要的朋友可以參考下2023-09-09

