golang 單元測試、基準(zhǔn)測試、子測試、并發(fā)測試基礎(chǔ)教程
一、go test基礎(chǔ)
用法: go test [build/test flags] [packages] [build/test flags & test binary flags]
Go test 會自動測試由導(dǎo)入路徑命名的軟件包。并以下格式打印測試結(jié)果的摘要:
ok archive/tar 0.011s
FAIL archive/zip 0.022s
ok compress/gzip 0.033s
...
Go test 會重新編譯每個軟件包以及名稱匹配的所有文件名形如 * _test.go 的測試文件。這些文件可以包含測試函數(shù),基準(zhǔn)測試函數(shù)和示例函數(shù)(test functions, benchmark functions, example functions)。更多有關(guān)信息,請參見“ go help testfunc”。
每個列出的包都執(zhí)行單獨的測試二進(jìn)制文件。其中名稱以“_”(包括“_test.go”)或“.”開頭的文件將被忽略。而后綴為“_test”的測試文件將被編譯為單獨的程序包,并與主測試文件鏈接并執(zhí)行。
go工具將忽略名為“testdata”的目錄,該目錄可用來存放測試所需的輔助數(shù)據(jù)。
作為構(gòu)建測試二進(jìn)制文件的一部分,go test 會對軟件包及其測試文件執(zhí)行run vet,用于檢測其中是否存在重大問題。如果執(zhí)行go vet階段發(fā)現(xiàn)任何問題,會直接報告這些問題,并且不運(yùn)行測試文件。檢查內(nèi)容只包括go vet檢查的一個子集,包括:'atomic', 'bool', 'buildtags', 'errorsas',
'ifaceassert', 'nilfunc', 'printf', and 'stringintconv'??梢酝ㄟ^“go doc cmd/vet”查看 vet檢查的相關(guān)文檔。如果要禁用go vet的運(yùn)行,請使用-vet=off標(biāo)志。
所有測試輸出和摘要行都打印到go命令的標(biāo)準(zhǔn)輸出,即使測試將其打印為自己的標(biāo)準(zhǔn)錯誤。(go命令的標(biāo)準(zhǔn)錯誤保留用于打印建立測試時出錯)。
Go test 以兩種不同的模式運(yùn)行:
第一種稱為本地目錄模式,執(zhí)行go test時無需指定package參數(shù)調(diào)用(例如,“go test”或“go test -v')。在這種模式下,go test會編譯當(dāng)前包,然后在當(dāng)前目錄中找到測試用例。緩存(如下所述)會被被禁用。包測試完成后,去打印摘要行顯示測試狀態(tài)(“ok”或“FAIL”),軟件包名稱和使用時間。
第二種稱為包列表模式,在調(diào)用go test時需要明確package參數(shù)(例如“go test math”,“go test./...”,或者“go test .”)。在這種模式下,將編譯并測試命令行上列出的每個軟件包。如果一個包測試通過,僅打印最終的“ok”摘要。如果包測試失敗,則打印完整的測試輸出。如果使用-bench或-v標(biāo)志調(diào)用,則go test會打印完整的輸出(包括通過測試的包),以顯示要求的基準(zhǔn)測試結(jié)果或詳細(xì)的日志記錄。在所有待測試包全部測試完畢,同時測試結(jié)果打印完成后,如果有任何一個測試未通過,則最后會打印一個’FAIL‘。
在第二種運(yùn)行模式下,go test會緩存成功的包測試結(jié)果,以避免不必要的重復(fù)測試。當(dāng)測試結(jié)果可以從緩存恢復(fù)時,執(zhí)行go test將顯示先前的測試結(jié)果。當(dāng)發(fā)生這種情況時,結(jié)果將輸出(cached)代替測試執(zhí)行時間這一參數(shù)。
二、準(zhǔn)備
接下來,將通過一個個例子說明單元測試、基準(zhǔn)測試、子測試、并發(fā)測試到底該如何寫,在此之前,我們需要先準(zhǔn)備一個待測試的小功能。這里,我們以一個對全局map變量執(zhí)行增刪改查的功能為例,開發(fā)對應(yīng)的測試代碼。具體如下:
var Cash = make(map[string]string)
func Add(key,value string){
if _,ok := Cash[key];!ok{
Cash[key] = value
}
}
func Delete(key string){
if _,ok := Cash[key];ok{
delete(Cash,key)
}
}
func Update(key,value string){
Cash[key] = value
}
func Get(key string) string{
if v,ok := Cash[key];ok{
return v
}
return ""
}
func Clean(){
Cash = make(map[string]string)
}
這里我們可以沒有為Cash變量添加鎖機(jī)制,目的是為了在驗證階段通過并發(fā)測試找出這個’bug‘。另外,還額外提供了一個Clean()方法,用于清空變量的內(nèi)容。
三、單元測試
單元測試是最簡單,也是最基本的測試。例如,我們想要測試add和get功能是否正常,通常我們會這樣寫。
func TestAddAndGet(t *testing.T){
Add("a","aa")
fmt.Println(Get("a"))
}
這是最基本的測試方法,但是效率太低,同時需要開發(fā)者自己去判斷結(jié)果是否正確。通常會通過Table-driven的方式實現(xiàn)這種簡單重復(fù)的測試內(nèi)容,我們可以按批次執(zhí)行測試,并通過程序化的方式驗證測試結(jié)果,具體如下:
func TestAdd(t *testing.T){
var addTests = []struct{
key string
value string
expected int
}{
{"a","aa",1},
{"b","bb",2},
{"c","cc",3},
{"c","cc",3},
{"c","cc",3},
{"d","dd",4},
{"e","ee",5},
{"f","ff",6},
{"g","gg",7},
{"h","hh",8},
{"i","ii",9},
{"j","jj",10},
}
quary := rand.Int()
for _,v := range addTests{
Add(v.key,v.value)
t.Logf("[goroutine:%d] add %s:%s",quary,v.key,v.value)
if len(Cash) != v.expected{
t.Errorf("add %s:%s len = %d; except %d",v.key,v.value,len(Cash),v.expected)
}
}
Clean()
}
如果某次插入操作出現(xiàn)異常會返回錯誤信息:
=== RUN TestAdd
basic_test.go:48: add d:dd len = 4; except 4
--- FAIL: TestAdd (0.00s)
FAIL
四、基準(zhǔn)測試
基準(zhǔn)測試,通常也被成為壓測Benchmark。壓測通常會自動的順序執(zhí)行多次,然后返回平均的壓測參數(shù)。例如我們測試get方法的性能:
func BenchmarkGet(b *testing.B) {
b.Log("start")
var addTests = []struct{
key string
value string
expected int
}{
{"a","aa",1},
{"b","bb",2},
{"c","cc",3},
{"c","cc",3},
{"c","cc",3},
{"d","dd",4},
{"e","ee",5},
{"f","ff",6},
{"g","gg",7},
{"h","hh",8},
{"i","ii",9},
{"j","jj",10},
}
for _,v := range addTests{
Add(v.key,v.value)
}
//啟動內(nèi)存統(tǒng)計
b.ReportAllocs()
//重新計時
b.ResetTimer()
for i:=0;i<b.N;i++{
var result []string
for _,v := range addTests{
value := Get(v.key)
if value != v.value{
b.Errorf("get %s:%s, except %s",v.key, value,v.value)
}
result = append(result,value)
}
}
}
測試結(jié)果為:
goos: darwin
goarch: amd64
pkg: test-learn
BenchmarkGet
basic_test.go:185: start
basic_test.go:185: start
basic_test.go:185: start
basic_test.go:185: start
basic_test.go:185: start
BenchmarkGet-12 2162974 546 ns/op 496 B/op 5 allocs/op
PASS
通過分析測試結(jié)果可知,測試流程共計執(zhí)行5次,測試結(jié)果為平均值。benchmark允許自定義測試計時的起止時間,默認(rèn)測試代碼啟動開始計時,測試結(jié)束停止計時。在本例中,由于我們想要測試get方法性能,因此排除前期數(shù)據(jù)插入時間。性能參數(shù)會更加準(zhǔn)確,因此我們通過b.ResetTimer()初始化了計時器開始的時間。
如果我們關(guān)注的流程在測試代碼前半段時呢?testing包提供了更加靈活的手動計時功能。
b.StartTimer()
b.StopTimer()
此外,testing支持內(nèi)存統(tǒng)計功能,b.ReportAllocs()相當(dāng)于在 go test 時添加-benchmem 標(biāo)識。
2162974 :基準(zhǔn)測試的迭代總次數(shù) b.N
546 ns/op:平均每次迭代所消耗的納秒數(shù)
496 B/op:平均每次迭代內(nèi)存所分配的字節(jié)數(shù)
5 allocs/op:平均每次迭代的內(nèi)存分配次數(shù)
五、并發(fā)測試
執(zhí)行并發(fā)測試有兩種方式,一種是單元并發(fā)測試,另一種是基準(zhǔn)并發(fā)測試。接下來我們將對兩種并發(fā)測試方式分別進(jìn)行介紹。再次之前我們需要先介紹一下 t.Parallel() 參數(shù),該參數(shù)指明當(dāng)前測試用例可與其他可并行執(zhí)行的測試用例一起運(yùn)行,僅僅聲明了測試用例的屬性,并不會真的去完成并發(fā)測試。
1、可并發(fā)執(zhí)行的測試用例
為了更清楚的說明可并發(fā)執(zhí)行t.Parallel() 屬性的意義,我們通過以下例子說明。假如我們有兩個測試插入數(shù)值的測試用例:
func TestCanParallelExecAdd(t *testing.T){
var addTests = []struct{
key string
value string
expected int
}{
{"a","aa",1},
{"b","bb",2},
{"c","cc",3},
{"c","cc",3},
{"c","cc",3},
{"d","dd",4},
{"e","ee",5},
{"f","ff",6},
{"g","gg",7},
{"h","hh",8},
{"i","ii",9},
{"j","jj",10},
}
t.Parallel()
quary := rand.Int()
t.Logf("[goroutine:%d] start",quary)
for _,v := range addTests{
Add(v.key,v.value)
t.Logf("[goroutine:%d] add %s:%s",quary,v.key,v.value)
if len(Cash) != v.expected{
t.Errorf("add %s:%s len = %d; except %d",v.key,v.value,len(Cash),v.expected)
}
}
Clean()
}
func TestCanParallelExecAdd2(t *testing.T){
var addTests = []struct{
key string
value string
expected int
}{
{"a","aa",1},
{"b","bb",2},
{"c","cc",3},
{"c","cc",3},
{"c","cc",3},
{"d","dd",4},
{"e","ee",5},
{"f","ff",6},
{"g","gg",7},
{"h","hh",8},
{"i","ii",9},
{"j","jj",10},
}
t.Parallel()
quary := rand.Int()
t.Logf("[goroutine:%d] start",quary)
for _,v := range addTests{
Add(v.key,v.value)
t.Logf("[goroutine:%d] add %s:%s",quary,v.key,v.value)
if len(Cash) != v.expected{
t.Errorf("add %s:%s len = %d; except %d",v.key,v.value,len(Cash),v.expected)
}
}
Clean()
}
這兩個測試用例除了名字,內(nèi)部實現(xiàn)完全一樣,然后我們執(zhí)行 go test 會返回錯誤: fatal error: concurrent map read and map write。 如果我們將t.Parallel()注釋,或者將其中一個測試用例注釋掉,將會通測試。
這是因為兩個測試用例都聲明了可并發(fā)執(zhí)行,當(dāng)我們執(zhí)行go test時,兩個測試用例將并發(fā)執(zhí)行,而map不是線程安全的數(shù)據(jù)結(jié)構(gòu),因此會爆出異常。
到這里,基準(zhǔn)并發(fā)測試的實現(xiàn)方式已經(jīng)十分清晰,但是這需要十分冗余的測試代碼,如果想要測試10并發(fā)量的測試難道要寫10份邏輯一樣的測試代碼嗎?
當(dāng)然不是,testing包提供了子測試的概念,可以便于我們實現(xiàn)更加復(fù)雜的測試邏輯。
2、基于子測試的并發(fā)單元測試
子測試是指我們可以在單元測試中啟動多個測試用例,具體如下:
func TestParallelAdd1(t *testing.T){
for i:=0;i<10;i++{
t.Run(fmt.Sprintf("g-%d",i), TestAdd)
}
}
我們通過t.run函數(shù),批量啟動了10個子測試用例。通過日志我們發(fā)現(xiàn),這10個子測試用例是按順序執(zhí)行的,這是因為TestAdd單元測試并不支持并發(fā)執(zhí)行,接下來我們對上述代碼做出修改,為子測試用例添加可并發(fā)屬性。
func TestParallelAdd(t *testing.T){
for i:=0;i<10;i++{
t.Run(fmt.Sprintf("g-%d",i), func(t *testing.T) {
t.Parallel()
TestAdd(t)
})
}
}
再次執(zhí)行,發(fā)生了fatal error: concurrent map writes的異常,測試未通過。
3、并發(fā)基準(zhǔn)測試
與單元測試不同,基準(zhǔn)測試提供了并發(fā)測試的方法b.RunParallel,具體如下:
func BenchmarkParallelAdd(b *testing.B) {
b.Log("start")
var process uint32 = 0
var count uint64 = 0
b.SetParallelism(2)
b.RunParallel(func(pb *testing.PB) {
temp := atomic.AddUint32(&process,1)
b.Logf("[goroutine:%d] start",temp)
for pb.Next() {
atomic.AddUint64(&count,1)
// The loop body is executed b.N times total across all goroutines.
b.Logf("[goroutine:%d] count=%d",temp,atomic.LoadUint64(&count))
}
b.Logf("[goroutine:%d] end",temp)
})
}
通過 RunParallel 方法能夠并行地執(zhí)行給定的基準(zhǔn)測試。RunParallel會創(chuàng)建出多個 goroutine,并將 b.N 分配給這些 goroutine 執(zhí)行,其中 goroutine 數(shù)量的默認(rèn)值為 GOMAXPROCS。用戶如果想要增加非 CPU 受限(non-CPU-bound)基準(zhǔn)測試的并行性,那么可以在 RunParallel 之前調(diào)用SetParallelism(如 SetParallelism(2),則 goroutine 數(shù)量為 2*GOMAXPROCS)。RunParallel 通常會與 -cpu 標(biāo)志一同使用。
六、示例功能
testing還提供了示例功能,一般用于展示某些功能的示例,此外也常用于測試。示例通常以example_test.go命名文件,示例函數(shù)通常以ExampleXxx_xxx命名,下劃線及其后內(nèi)容不是必須的。具體如下:
func ExampleGet() {
Add("a","aa")
Add("b","bb")
fmt.Println(Get("a"))
fmt.Println(Get("b"))
// Output:
// aa
// bb
}
七、總結(jié)
通常測試用例文件被建議與源文件寫在同一個包中。
測試常用命令
go test執(zhí)行當(dāng)前包中全部測試用例,不包括 benchmark測試
go test -bench=.執(zhí)行當(dāng)前包中全部測試用例,包括 benchmark測試
go test -v執(zhí)行測試用例時打印測試詳情
go test -race檢查當(dāng)前測試代碼是否存在競爭異常,用于檢查線程安全
go test -cover檢查測試代碼覆蓋率