GO語言測試


Go語言的測試技術是相對低級的。它依賴一個 go test 測試命令和一組按照約定方式編寫的 測試函數,測試命令可以運行這些測試函數。編寫相對輕量級的純測試代碼是有效的,而且它很容易延伸到基准測試和示例文檔。

go test

編寫測試代碼和編寫普通的Go代碼過程是類似的,並不需要學習新的語法、規則或工具。
在包目錄內,所有以_test.go為后綴名的源代碼文件都是go test測試的一部分,不會被go build編譯到最終的可執行文件中。

類型 格式 作用
測試函數 函數名前綴為Test 測試程序的一些邏輯行為是否正確
基准函數 函數名前綴為Benchmark 測試函數的性能
示例函數 函數名前綴為Example 為文檔提供示例文檔

go test命令會遍歷所有的*_test.go文件中符合上述命名規則的函數,然后生成一個臨時的main包用於調用相應的測試函數,然后構建並運行、報告測試結果,最后清理測試中生成的臨時文件。

測試函數

格式:測試函數的名字必須以Test開頭,可選的后綴名必須以大寫字母開頭,參數t用於報告測試失敗和附加的日志信息。

func TestName(t *testing.T){
    // ...
}

testing.T的擁有的方法:

func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool

示例:自定義一個split函數,實現split功能:

func Split(s,sep string) (result []string)  {
    i := strings.Index(s,sep)
    for i > -1{
        result = append(result,s[:i])
        s = s[i+1:]
        i = strings.Index(s,sep)
    }
    result =append(result,s)
    return 
}

在當前目錄下,創建一個split_test.go的測試文件,並定義一個測試函數如下:

func TsetSplit(t *testing.T)  {
    got := Split("a:b:c",":")
    want := []string{"a","b","c"}
    if !reflect.DeepEqual(want,got){
        t.Errorf("期待:%v,得到:%v",want,got)
    }
}

在split包路徑下,執行go test命令,可以看到輸出結果如下:

testing: warning: no tests to run
PASS

出現pass意味測試通過!
如果有多個測試函數,其中一個測試不過,會出現以下提示:

--- FAIL: TestMoreSplit (0.00s)
    split_test.go:40: excepted:[a d], got:[a cd]
FAIL
exit status 1

如果要查看測試函數名稱和運行時間,可以使用go test -v

=== RUN   TestMoreSplit
--- FAIL: TestMoreSplit (0.00s)
    split_test.go:40: excepted:[a d], got:[a cd]
FAIL

還可以在go test命令后添加-run參數,它對應一個正則表達式,只有函數名匹配上的測試函數才會被go test命令執行。

測試組

如果有多個測試,不必每個測試都寫一個函數,可以使用測試組來完成。

//測試組
func TestSplit(t *testing.T) {
    type test []struct{
        input string
        sep string
        want []string
    }
    tests := test{
        {input:"a:b:c",sep:":",want:[]string{"a","b","c"}},
        {input:"a:b:c",sep:",",want:[]string{"a","b","c"}},
        {input:"abcdbce",sep:"bc",want:[]string{"a","d","e"}},
        {input:"上海自來水來自海上",sep:"海",want:[]string{"上","自來水來自","上"}},
    }
    for _,tc := range tests{
        got := Split(tc.input,tc.sep)
        if !reflect.DeepEqual(got,tc.want){
            t.Errorf("期待:%v,獲得:%v",tc.want,got)
        }
    }
}

執行go test -v,獲得

=== RUN   TestSplit
--- FAIL: TestSplit (0.00s)
    split_test.go:59: 期待:[a b c],獲得:[a:b:c]
    split_test.go:59: 期待:[a d e],獲得:[a cd ce]
    split_test.go:59: 期待:[上 自來水來自 上],獲得:[上 ��自來水來自 ��上]
FAIL
exit status 1

中文有亂碼,這種情況下可以使用%#v的格式化方式查看亂碼是什么:
t.Errorf("期待:%#v,獲得:%#v",tc.want,got)
得到:
split_test.go:59: 期待:[]string{"上", "自來水來自", "上"},獲得:[]string{"上", "\xb5\xb7自來水來自", "\xb5\xb7上"}

當測試組中測試比較多的時候,可以加個name來查看是哪個測試用例出了問題:

//測試組
func TestSplit(t *testing.T) {
    type test struct{  // 定義test結構體
        input string
        sep string
        want []string
    }
    tests := map[string]test{
        "simple":   {input:"a:b:c",sep:":",want:[]string{"a","b","c"}},
        "simple1":  {input:"a:b:c",sep:",",want:[]string{"a","b","c"}},
        "simple2":  {input:"abcdbce",sep:"bc",want:[]string{"a","d","e"}},
        "simple3":  {input:"上海自來水來自海上",sep:"海",want:[]string{"上","自來水來自","上"}},

    }
    for name,tc := range tests{
        got := Split(tc.input,tc.sep)
        if !reflect.DeepEqual(got,tc.want){
            t.Errorf("測試名:%#v,期待:%#v,獲得:%#v",name,tc.want,got)
        }
    }
}

輸出的時候就會把測試用例的名字帶上:

=== RUN   TestSplit
--- FAIL: TestSplit (0.00s)
    split_test.go:66: 測試名:"simple1",期待:[]string{"a", "b", "c"},獲得:[]string{"a:b:c"}
    split_test.go:66: 測試名:"simple2",期待:[]string{"a", "d", "e"},獲得:[]string{"a", "cd", "ce"}
    split_test.go:66: 測試名:"simple3",期待:[]string{"上", "自來水來自", "上"},獲得:[]string{"上", "\xb5\xb7自來水來自", "\xb5\xb7上"}
FAIL
exit status 1

子測試

    for name,tc := range tests{
        t.Run(name, func(t *testing.T) {
            got := Split(tc.input,tc.sep)
            if !reflect.DeepEqual(got,tc.want){
                t.Errorf("測試名:%#v,期待:%#v,獲得:%#v",name,tc.want,got)
            }
        })
    }

執行結果:

$go test -v
=== RUN   TestSplit
=== RUN   TestSplit/simple2
=== RUN   TestSplit/simple3
=== RUN   TestSplit/simple
=== RUN   TestSplit/simple1
--- FAIL: TestSplit (0.00s)
    --- FAIL: TestSplit/simple2 (0.00s)
        split_test.go:71: 測試名:"simple2",期待:[]string{"a", "d", "e"},獲得:[]string{"a", "cd", "ce"}
    --- FAIL: TestSplit/simple3 (0.00s)
        split_test.go:71: 測試名:"simple3",期待:[]string{"上", "自來水來自", "上"},獲得:[]string{"上", "\xb5\xb7自來水來自", "\xb5\xb7上"}
    --- PASS: TestSplit/simple (0.00s)
    --- FAIL: TestSplit/simple1 (0.00s)
        split_test.go:71: 測試名:"simple1",期待:[]string{"a", "b", "c"},獲得:[]string{"a:b:c"}
FAIL
exit status 1

可以通過-run=RegExp來指定運行的測試用例,還可以通過/來指定要運行的子測試用例,只測試simple:
$go test -v -run=Split/simple

測試覆蓋率

在測試中至少被運行一次的代碼占總代碼的比例。Go提供內置功能來檢查你的代碼覆蓋率。我們可以使用go test -cover來查看測試覆蓋率。

$go test -cover
--- FAIL: TestSplit (0.00s)
    --- FAIL: TestSplit/simple2 (0.00s)
        split_test.go:71: 測試名:"simple2",期待:[]string{"a", "d", "e"},獲得:[]string{"a", "cd", "ce"}
    --- FAIL: TestSplit/simple3 (0.00s)
        split_test.go:71: 測試名:"simple3",期待:[]string{"上", "自來水來自", "上"},獲得:[]string{"上", "\xb5\xb7自來水來自", "\xb5\xb7上"}
    --- FAIL: TestSplit/simple1 (0.00s)
        split_test.go:71: 測試名:"simple1",期待:[]string{"a", "b", "c"},獲得:[]string{"a:b:c"}
FAIL
coverage: 100.0% of statements
exit status 1

Go還提供了一個額外的-coverprofile參數,用來將覆蓋率相關的記錄信息輸出到一個文件。
$ go test -cover -coverprofile=c.out
執行go tool cover -html=c.out,使用cover工具來處理生成的記錄信息,該命令會打開本地的瀏覽器窗口生成一個HTML報告。

用綠色標記的語句塊表示被覆蓋了,而紅色的表示沒有被覆蓋。

基准測試

基准測試就是在一定的工作負載之下檢測程序性能的一種方法。
格式:

func BenchmarkName(b *testing.B){
    // ...
}

基准測試以Benchmark為前綴,需要一個'*testing.B'類型的參數b,基准測試必須要執行b.N次,這樣的測試才有對照性,b.N的值是系統根據實際情況去調整的,從而保證測試的穩定性。 testing.B擁有的方法如下:

func (c *B) Error(args ...interface{})
func (c *B) Errorf(format string, args ...interface{})
func (c *B) Fail()
func (c *B) FailNow()
func (c *B) Failed() bool
func (c *B) Fatal(args ...interface{})
func (c *B) Fatalf(format string, args ...interface{})
func (c *B) Log(args ...interface{})
func (c *B) Logf(format string, args ...interface{})
func (c *B) Name() string
func (b *B) ReportAllocs()
func (b *B) ResetTimer()
func (b *B) Run(name string, f func(b *B)) bool
func (b *B) RunParallel(body func(*PB))
func (b *B) SetBytes(n int64)
func (b *B) SetParallelism(p int)
func (c *B) Skip(args ...interface{})
func (c *B) SkipNow()
func (c *B) Skipf(format string, args ...interface{})
func (c *B) Skipped() bool
func (b *B) StartTimer()
func (b *B) StopTimer()

為split編寫基准測試:

//基准測試
func BenchmarkSplit(b *testing.B) {
    for i:=0;i<b.N;i++{
        Split("a:b:c",":")
    }
}

通過執行go test -bench=Split命令執行基准測試,輸出結果如下:

$go test -bench=Split
goos: darwin
goarch: amd64
pkg: xxxxxx
BenchmarkSplit-8        10000000               203 ns/op
PASS

其中BenchmarkSplit-8表示對Split函數進行基准測試,數字8表示GOMAXPROCS的值,這個對於並發基准測試很重要。10000000和203ns/op表示每次調用Split函數耗時203ns,這個結果是10000000次調用的平均值。
默認情況下,每個基准測試至少運行1秒!
可為基准測試添加-benchmem參數,來獲得內存分配的統計數據。

$go test -bench=Split -benchmem
...
BenchmarkSplit-8        10000000               203 ns/op             112 B/op          3 allocs/op
...

112 B/op表示每次操作內存分配了112字節,3 allocs/op則表示每次操作進行了3次內存分配。

性能比較函數

性能比較函數通常是一個帶有參數的函數,被多個不同的Benchmark函數傳入不同的值來調用。

func benchmark(b *testing.B, size int){/* ... */}
func Benchmark10(b *testing.B){ benchmark(b, 10) }
func Benchmark100(b *testing.B){ benchmark(b, 100) }
func Benchmark1000(b *testing.B){ benchmark(b, 1000) }

使用斐波那契數的函數測試:

// Fib 是一個計算第n個斐波那契數的函數
func Fib(n int) int  {
    if n <2{
        return n
    }
    return Fib(n-1) + Fib(n+2)
}

性能比較函數:

func benchmarkFib(b *testing.B, n int) {
    for i := 0; i < b.N; i++ {
        Fib(n)
    }
}
func BenchmarkFib1(b *testing.B)  { benchmarkFib(b, 1) }
func BenchmarkFib2(b *testing.B)  { benchmarkFib(b, 2) }
func BenchmarkFib3(b *testing.B)  { benchmarkFib(b, 3) }
func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) }
func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) }
func BenchmarkFib40(b *testing.B) { benchmarkFib(b, 40) }

默認情況下,每個基准測試至少運行1秒。如果在Benchmark函數返回時沒有到1秒,則b.N的值會按1,2,5,10,20,50,…增加,並且函數再次運行。
還可以使用-benchtime標志增加最小基准時間,以產生更准確的結果。

並行測試

func (b *B) RunParallel(body func(*PB))會以並行的方式執行給定的基准測試。 RunParallel會創建出多個goroutine,並將b.N分配給這些 goroutine 執行, 其中 goroutine數量的默認值為GOMAXPROCS。用戶如果想要增加非CPU受限(non-CPU-bound)基准測試的並行性, 那么可以在RunParallel之前調用SetParallelism 。RunParallel通常會與-cpu標志一同使用。
go test -bench=. -cpu 1來指定使用的CPU數量。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM