一篇文章讓你學(xué)會寫golang 單元測試、基準(zhǔn)測試、子測試、并發(fā)測試

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 檢查測試代碼覆蓋率

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容