Go基礎:如何做單元測試和基準測試

目錄

1. 單元測試

1.1. go test工具

go test的參數解讀:

1.2. 測試函數

1.2.1. 測試函數的格式

1.2.2. 測試函數示例

1.3. 測試組

1.4. 子測試 t.Run

1.5. 測試覆蓋率 go test -cover

1.6. 基準測試--Benchmark

1.6.1. 基準測試函數格式

1.6.2. 基準測試示例

1.6.3. 性能比較函數

1.6.4. 重置時間 ResetTimer() 

1.6.5. 並行測試 RunParallel

1.6.6. 結合 pprof 性能監控

1.7. Setup與TearDown

1.7.1. TestMain

1.7.2. 子測試的Setup與Teardown

1.8. 示例函數 Example

1.8.1. 示例函數的格式

1.8.2. 示例函數示例


1. 單元測試

不寫測試的開發不是好程序員。我個人非常崇尚TDD(Test Driven Development)的,然而可惜的是國內的程序員都不太關注測試這一部分。 這篇文章主要介紹下在Go語言中如何做單元測試和基準測試。

1.1. go test工具

Go語言中的測試依賴go test命令。編寫測試代碼和編寫普通的Go代碼過程是類似的,並不需要學習新的語法、規則或工具。

  • go test命令是一個按照一定約定和組織的測試代碼的驅動程序。
  • 在包目錄內,所有以_test.go爲後綴名的源代碼文件都是go test測試的一部分,不會被go build編譯到最終的可執行文件中。

*_test.go文件中有三種類型的函數,單元測試函數、基準測試函數和示例函數。

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

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

Golang單元測試對文件名和方法名,參數都有很嚴格的要求。

    1、文件名必須以xx_test.go命名
    2、方法必須是Test[^a-z]開頭
    3、方法參數必須 t *testing.T
    4、使用go test執行單元測試

go test的參數解讀:

go test是go語言自帶的測試工具,其中包含的是兩類,單元測試和性能測試

通過go help test可以看到go test的使用說明:

格式形如: go test [-c] [-i] [build flags] [packages] [flags for test binary]

參數解讀:

-c : 編譯go test成爲可執行的二進制文件,但是不運行測試。

-i : 安裝測試包依賴的package,但是不運行測試。

關於build flags,調用go help build,這些是編譯運行過程中需要使用到的參數,一般設置爲空

關於packages,調用go help packages,這些是關於包的管理,一般設置爲空

關於flags for test binary,調用,這些是go test過程中經常使用到的參數

  1. -test.v : 是否輸出全部的單元測試用例(不管成功或者失敗),默認沒有加上,所以只輸出失敗的單元測試用例。
  2. -test.run pattern: 只跑哪些單元測試用例
  3. -test.bench patten: 只跑那些性能測試用例
  4. -test.benchmem : 是否在性能測試的時候輸出內存情況
  5. -test.benchtime t : 性能測試運行的時間,默認是1s
  6. -test.cpuprofile cpu.out : 是否輸出cpu性能分析文件
  7. -test.memprofile mem.out : 是否輸出內存性能分析文件
  8. -test.blockprofile block.out : 是否輸出內部goroutine阻塞的性能分析文件
  9. -test.memprofilerate n : 內存性能分析的時候有一個分配了多少的時候纔打點記錄的問題。這個參數就是設置打點的內存分配間隔,也就是profile中一個sample代表的內存大小。默認是設置爲512 * 1024的。如果你將它設置爲1,則每分配一個內存塊就會在profile中有個打點,那麼生成的profile的sample就會非常多。如果你設置爲0,那就是不做打點了。你可以通過設置memprofilerate=1和GOGC=off來關閉內存回收,並且對每個內存塊的分配進行觀察。
  10. -test.blockprofilerate n: 基本同上,控制的是goroutine阻塞時候打點的納秒數。默認不設置就相當於-test.blockprofilerate=1,每一納秒都打點記錄一下
  11. -test.parallel n : 性能測試的程序並行cpu數,默認等於GOMAXPROCS。
  12. -test.timeout t : 如果測試用例運行時間超過t,則拋出panic
  13. -test.cpu 1,2,4 : 程序運行在哪些CPU上面,使用二進制的1所在位代表,和nginx的nginx_worker_cpu_affinity是一個道理
  14. -test.short : 將那些運行時間較長的測試用例運行時間縮短

目錄結構:

    test
      |
       —— calc.go
      |
       —— calc_test.go

1.2. 測試函數

1.2.1. 測試函數的格式

每個測試函數必須導入testing包,測試函數的基本格式(簽名)如下:

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

測試函數的名字必須以Test開頭,可選的後綴名必須以大寫字母開頭,舉幾個例子:\

func TestAdd(t *testing.T){ ... }
func TestSum(t *testing.T){ ... }
func TestLog(t *testing.T){ ... }

其中參數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

1.2.2. 測試函數示例

就像細胞是構成我們身體的基本單位,一個軟件程序也是由很多單元組件構成的。單元組件可以是函數、結構體、方法和最終用戶可能依賴的任意東西。總之我們需要確保這些組件是能夠正常運行的。

單元測試是一些利用各種方法測試單元組件的程序,它會將結果與預期輸出進行比較。

接下來,我們定義一個split的包,包中定義了一個Split函數,具體實現如下:

// split/split.go

package split

import "strings"

// split package with a single split function.

// Split slices s into all substrings separated by sep and
// returns a slice of the substrings between those separators.
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的測試文件,並定義一個測試函數如下:

// split/split_test.go

package split

import (
    "reflect"
    "testing"
)

func TestSplit(t *testing.T) { // 測試函數名必須以Test開頭,必須接收一個*testing.T類型參數
    got := Split("a:b:c", ":")         // 程序輸出的結果
    want := []string{"a", "b", "c"}    // 期望的結果
    if !reflect.DeepEqual(want, got) { // 因爲slice不能比較直接,藉助反射包中的方法比較
        t.Errorf("excepted:%v, got:%v", want, got) // 測試失敗輸出錯誤提示
    }
}

此時split這個包中的文件如下:

    split $ ls -l
    total 16
    -rw-r--r--  1 pprof staff  408  4 29 15:50 split.go
    -rw-r--r--  1 pprof  staff  466  4 29 16:04 split_test.go

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

split $ go test
PASS
ok      github.com/pprof/studygo/code_demo/test_demo/split       0.005s

一個測試用例有點單薄,我們再編寫一個測試使用多個字符切割字符串的例子,在split_test.go中添加如下測試函數:

func TestMoreSplit(t *testing.T) {
    got := Split("abcd", "bc")
    want := []string{"a", "d"}
    if !reflect.DeepEqual(want, got) {
        t.Errorf("excepted:%v, got:%v", want, got)
    }
}

再次運行go test命令,輸出結果如下:

    split $ go test
    --- FAIL: TestMultiSplit (0.00s)
        split_test.go:20: excepted:[a d], got:[a cd]
    FAIL
    exit status 1
    FAIL    github.com/pprof/studygo/code_demo/test_demo/split       0.006s

這一次,我們的測試失敗了。我們可以爲go test命令添加-v參數,查看測試函數名稱和運行時間:

    split $ go test -v
    === RUN   TestSplit
    --- PASS: TestSplit (0.00s)
    === RUN   TestMoreSplit
    --- FAIL: TestMoreSplit (0.00s)
        split_test.go:21: excepted:[a d], got:[a cd]
    FAIL
    exit status 1
    FAIL    github.com/pprof/studygo/code_demo/test_demo/split       0.005s

這一次我們能清楚的看到是TestMoreSplit這個測試沒有成功。

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

    split $ go test -v -run="More"
    === RUN   TestMoreSplit
    --- FAIL: TestMoreSplit (0.00s)
        split_test.go:21: excepted:[a d], got:[a cd]
    FAIL
    exit status 1
    FAIL    github.com/pprof/studygo/code_demo/test_demo/split       0.006s

現在我們回過頭來解決我們程序中的問題。很顯然我們最初的split函數並沒有考慮到sep爲多個字符的情況,我們來修復下這個Bug:

package split

import "strings"

// split package with a single split function.

// Split slices s into all substrings separated by sep and
// returns a slice of the substrings between those separators.
func Split(s, sep string) (result []string) {
    i := strings.Index(s, sep)

    for i > -1 {
        result = append(result, s[:i])
        s = s[i+len(sep):] // 這裏使用len(sep)獲取sep的長度
        i = strings.Index(s, sep)
    }
    result = append(result, s)
    return
}

這一次我們再來測試一下,我們的程序。注意,當我們修改了我們的代碼之後不要僅僅執行那些失敗的測試函數,我們應該完整的運行所有的測試,保證不會因爲修改代碼而引入了新的問題。

    split $ go test -v
    === RUN   TestSplit
    --- PASS: TestSplit (0.00s)
    === RUN   TestMoreSplit
    --- PASS: TestMoreSplit (0.00s)
    PASS
    ok      github.com/pprof/studygo/code_demo/test_demo/split       0.006s

這一次我們的測試都通過了

1.3. 測試組

我們現在還想要測試一下split函數對中文字符串的支持,這個時候我們可以再編寫一個TestChineseSplit測試函數,但是我們也可以使用如下更友好的一種方式來添加更多的測試用例。

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: "abcd", sep: "bc", want: []string{"a", "d"}},
        {input: "枯藤老樹昏鴉", sep: "老", want: []string{"枯藤", "樹昏鴉"}},
    }
    // 遍歷切片,逐一執行測試用例
    for _, tc := range tests {
        got := Split(tc.input, tc.sep)
        if !reflect.DeepEqual(got, tc.want) {
            t.Errorf("excepted:%v, got:%v", tc.want, got)
        }
    }
}

我們通過上面的代碼把多個測試用例合到一起,再次執行go test命令。

    split $ go test -v
    === RUN   TestSplit
    --- FAIL: TestSplit (0.00s)
        split_test.go:42: excepted:[枯藤 樹昏鴉], got:[ 枯藤 樹昏鴉]
    FAIL
    exit status 1
    FAIL    github.com/pprof/studygo/code_demo/test_demo/split       0.006s

我們的測試出現了問題,仔細看打印的測試失敗提示信息:excepted:[枯藤 樹昏鴉], got:[ 枯藤 樹昏鴉],你會發現[ 枯藤 樹昏鴉]中有個不明顯的空串,這種情況下十分推薦使用%#v的格式化方式。

我們修改下測試用例的格式化輸出錯誤提示部分:

func TestSplit(t *testing.T) {
   ...

    for _, tc := range tests {
        got := Split(tc.input, tc.sep)
        if !reflect.DeepEqual(got, tc.want) {
            t.Errorf("excepted:%#v, got:%#v", tc.want, got)
        }
    }
}

此時運行go test命令後就能看到比較明顯的提示信息了:

    split $ go test -v
    === RUN   TestSplit
    --- FAIL: TestSplit (0.00s)
        split_test.go:42: excepted:[]string{"枯藤", "樹昏鴉"}, got:[]string{"", "枯藤", "樹昏鴉"}
    FAIL
    exit status 1
    FAIL    github.com/Q1mi/studygo/code_demo/test_demo/split       0.006s

1.4. 子測試 t.Run

看起來都挺不錯的,但是如果測試用例比較多的時候,我們是沒辦法一眼看出來具體是哪個測試用例失敗了。我們可能會想到下面的解決辦法

func TestSplit(t *testing.T) {
    type test struct { // 定義test結構體
        input string
        sep   string
        want  []string
    }
    tests := map[string]test{ // 測試用例使用map存儲
        "simple":      {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
        "wrong sep":   {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
        "more sep":    {input: "abcd", sep: "bc", want: []string{"a", "d"}},
        "leading sep": {input: "枯藤老樹昏鴉", sep: "老", want: []string{"枯藤", "樹昏鴉"}},
    }
    for name, tc := range tests {
        got := Split(tc.input, tc.sep)
        if !reflect.DeepEqual(got, tc.want) {
            t.Errorf("name:%s excepted:%#v, got:%#v", name, tc.want, got) // 將測試用例的name格式化輸出
        }
    }
}

上面的做法是能夠解決問題的。同時Go1.7+中新增了子測試,我們可以按照如下方式使用t.Run執行子測試:

func TestSplit(t *testing.T) {
    type test struct { // 定義test結構體
        input string
        sep   string
        want  []string
    }
    tests := map[string]test{ // 測試用例使用map存儲
        "simple":      {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
        "wrong sep":   {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
        "more sep":    {input: "abcd", sep: "bc", want: []string{"a", "d"}},
        "leading sep": {input: "枯藤老樹昏鴉", sep: "老", want: []string{"枯藤", "樹昏鴉"}},
    }
    for name, tc := range tests {
        t.Run(name, func(t *testing.T) { // 使用t.Run()執行子測試
            got := Split(tc.input, tc.sep)
            if !reflect.DeepEqual(got, tc.want) {
                t.Errorf("excepted:%#v, got:%#v", tc.want, got)
            }
        })
    }
}

此時我們再執行go test命令就能夠看到更清晰的輸出內容了:

    split $ go test -v
    === RUN   TestSplit
    === RUN   TestSplit/leading_sep
    === RUN   TestSplit/simple
    === RUN   TestSplit/wrong_sep
    === RUN   TestSplit/more_sep
    --- FAIL: TestSplit (0.00s)
        --- FAIL: TestSplit/leading_sep (0.00s)
            split_test.go:83: excepted:[]string{"枯藤", "樹昏鴉"}, got:[]string{"", "枯藤", "樹昏鴉"}
        --- PASS: TestSplit/simple (0.00s)
        --- PASS: TestSplit/wrong_sep (0.00s)
        --- PASS: TestSplit/more_sep (0.00s)
    FAIL
    exit status 1
    FAIL    github.com/pprof/studygo/code_demo/test_demo/split       0.006s

這個時候我們要把測試用例中的錯誤修改回來:

func TestSplit(t *testing.T) {
    ...
    tests := map[string]test{ // 測試用例使用map存儲
        "simple":      {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
        "wrong sep":   {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
        "more sep":    {input: "abcd", sep: "bc", want: []string{"a", "d"}},
        "leading sep": {input: "枯藤老樹昏鴉", sep: "老", want: []string{"", "枯藤", "樹昏鴉"}},
    }
    ...
}

我們都知道可以通過-run=RegExp來指定運行的測試用例,還可以通過/來指定要運行的子測試用例,例如:go test -v -run=Split/simple只會運行simple對應的子測試用例。

1.5. 測試覆蓋率 go test -cover

測試覆蓋率是你的代碼被測試套件覆蓋的百分比。通常我們使用的都是語句的覆蓋率,也就是在測試中至少被運行一次的代碼佔總代碼的比例。

Go提供內置功能來檢查你的代碼覆蓋率。我們可以使用go test -cover來查看測試覆蓋率。例如:

    split $ go test -cover
    PASS
    coverage: 100.0% of statements
    ok      github.com/pprof/studygo/code_demo/test_demo/split       0.005s

從上面的結果可以看到我們的測試用例覆蓋了100%的代碼。

Go還提供了一個額外的-coverprofile參數,用來將覆蓋率相關的記錄信息輸出到一個文件。

例如:

    split $ go test -cover -coverprofile=c.out
    PASS
    coverage: 100.0% of statements
    ok      github.com/pprof/studygo/code_demo/test_demo/split       0.005s

上面的命令會將覆蓋率相關的信息輸出到當前文件夾下面的c.out文件中,然後我們執行go tool cover -html=c.out,使用cover工具來處理生成的記錄信息,該命令會打開本地的瀏覽器窗口生成一個HTML報告。

1.6. 基準測試--Benchmark

基準測試--Benchmark

1.6.1. 基準測試函數格式

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

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()

1.6.2. 基準測試示例

我們爲split包中的Split函數編寫基準測試如下:

func BenchmarkSplit(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Split("枯藤老樹昏鴉", "老")
    }
}

基準測試並不會默認執行,需要增加-bench參數,所以我們通過執行go test -bench=Split命令執行基準測試,輸出結果如下:

    split $ go test -bench=Split
    goos: darwin
    goarch: amd64
    pkg: github.com/pprof/studygo/code_demo/test_demo/split
    BenchmarkSplit-8        10000000               203 ns/op
    PASS
    ok      github.com/pprof/studygo/code_demo/test_demo/split       2.255s
  • 其中BenchmarkSplit-8表示對Split函數進行基準測試,數字8表示GOMAXPROCS的值,這個對於併發基準測試很重要。
  • 10000000和203ns/op表示每次調用Split函數耗時203ns,這個結果是10000000次調用的平均值。

我們還可以爲基準測試添加-benchmem參數,來獲得內存分配的統計數據

    split $ go test -bench=Split -benchmem
    goos: darwin
    goarch: amd64
    pkg: github.com/pprof/studygo/code_demo/test_demo/split
    BenchmarkSplit-8        10000000               215 ns/op             112 B/op          3 allocs/op
    PASS
    ok      github.com/pprof/studygo/code_demo/test_demo/split       2.394s

其中,112 B/op表示每次操作內存分配了112字節,3 allocs/op則表示每次操作進行了3次內存分配。 我們將我們的Split函數優化如下:

func Split(s, sep string) (result []string) {
    result = make([]string, 0, strings.Count(s, sep)+1)
    i := strings.Index(s, sep)
    for i > -1 {
        result = append(result, s[:i])
        s = s[i+len(sep):] // 這裏使用len(sep)獲取sep的長度
        i = strings.Index(s, sep)
    }
    result = append(result, s)
    return
}

這一次我們提前使用make函數將result初始化爲一個容量足夠大的切片,而不再像之前一樣通過調用append函數來追加。我們來看一下這個改進會帶來多大的性能提升:

    split $ go test -bench=Split -benchmem
    goos: darwin
    goarch: amd64
    pkg: github.com/pprof/studygo/code_demo/test_demo/split
    BenchmarkSplit-8        10000000               127 ns/op              48 B/op          1 allocs/op
    PASS
    ok      github.com/pprof/studygo/code_demo/test_demo/split       1.423s

這個使用make函數提前分配內存的改動,減少了2/3的內存分配次數,並且減少了一半的內存分配。

1.6.3. 性能比較函數

上面的基準測試只能得到給定操作的絕對耗時,但是在很多性能問題是

  • 發生在兩個不同操作之間的相對耗時,比如同一個函數處理1000個元素的耗時與處理1萬甚至100萬個元素的耗時的差別是多少?
  • 再或者對於同一個任務究竟使用哪種算法性能最佳?

我們通常需要對兩個不同算法的實現使用相同的輸入來進行基準比較測試。

性能比較函數通常是一個帶有參數的函數,被多個不同的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.go

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

我們編寫的性能比較函數如下:

// fib_test.go

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) }

運行基準測試:

    split $ go test -bench=.
    goos: darwin
    goarch: amd64
    pkg: github.com/pprof/studygo/code_demo/test_demo/fib
    BenchmarkFib1-8         1000000000               2.03 ns/op
    BenchmarkFib2-8         300000000                5.39 ns/op
    BenchmarkFib3-8         200000000                9.71 ns/op
    BenchmarkFib10-8         5000000               325 ns/op
    BenchmarkFib20-8           30000             42460 ns/op
    BenchmarkFib40-8               2         638524980 ns/op
    PASS
    ok      github.com/pprof/studygo/code_demo/test_demo/fib 12.944s

這裏需要注意的是,默認情況下,每個基準測試至少運行1秒。如果在Benchmark函數返回時沒有到1秒,則b.N的值會按1,2,5,10,20,50,…增加,並且函數再次運行。

最終的BenchmarkFib40只運行了兩次,每次運行的平均值只有不到一秒。像這種情況下我們應該可以使用-benchtime標誌增加最小基準時間,以產生更準確的結果。例如:

    split $ go test -bench=Fib40 -benchtime=20s
    goos: darwin
    goarch: amd64
    pkg: github.com/pprof/studygo/code_demo/test_demo/fib
    BenchmarkFib40-8              50         663205114 ns/op
    PASS
    ok      github.com/pprof/studygo/code_demo/test_demo/fib 33.849s

這一次BenchmarkFib40函數運行了50次,結果就會更準確一些了。

使用性能比較函數做測試的時候一個容易犯的錯誤就是把b.N作爲輸入的大小,例如以下兩個例子都是錯誤的示範:

// 錯誤示範1
func BenchmarkFibWrong(b *testing.B) {
    for n := 0; n < b.N; n++ {
        Fib(n)
    }
}

// 錯誤示範2
func BenchmarkFibWrong2(b *testing.B) {
    Fib(b.N)
}

1.6.4. 重置時間 ResetTimer() 

b.ResetTimer之前的處理不會放到執行時間裏,也不會輸出到報告中,所以可以在之前做一些不計劃作爲測試報告的操作。例如:

func BenchmarkSplit(b *testing.B) {
    time.Sleep(5 * time.Second) // 假設需要做一些耗時的無關操作
    b.ResetTimer()              // 重置計時器
    for i := 0; i < b.N; i++ {
        Split("枯藤老樹昏鴉", "老")
    }
}

1.6.5. 並行測試 RunParallel

func (b B) RunParallel(body func(PB))會以並行的方式執行給定的基準測試。

  • RunParallel會創建出多個goroutine,並將b.N分配給這些goroutine執行, 其中goroutine數量的默認值爲GOMAXPROCS
  • 用戶如果想要增加非CPU受限(non-CPU-bound)基準測試的並行性, 那麼可以在RunParallel之前調用SetParallelism
  • RunParallel通常會與-cpu標誌一同使用。
func BenchmarkSplitParallel(b *testing.B) {
    // b.SetParallelism(1) // 設置使用的CPU數
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            Split("枯藤老樹昏鴉", "老")
        }
    })
}

執行一下基準測試:

split $ go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/pprof/studygo/code_demo/test_demo/split
BenchmarkSplit-8                10000000               131 ns/op
BenchmarkSplitParallel-8        50000000                36.1 ns/op
PASS
ok      github.com/pprof/studygo/code_demo/test_demo/split       3.308s

還可以通過在測試命令後添加-cpu參數如go test -bench=. -cpu 1來指定使用的CPU數量。

1.6.6. 結合 pprof 性能監控

package bench
import "testing"
func Fib(n int) int {
    if n < 2 {
      return n
    }
    return Fib(n-1) + Fib(n-2)
}
func BenchmarkFib10(b *testing.B) {
    // run the Fib function b.N times
    for n := 0; n < b.N; n++ {
      Fib(10)
    }
}
go test -bench=. -benchmem -cpuprofile profile.out

還可以同時看內存

go test -bench=. -benchmem -memprofile memprofile.out -cpuprofile profile.out

然後就可以用輸出的文件使用pprof

go tool pprof profile.out
File: bench.test
Type: cpu
Time: Apr 5, 2018 at 4:27pm (EDT)
Duration: 2s, Total samples = 1.85s (92.40%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 1.85s, 100% of 1.85s total
      flat  flat%   sum%        cum   cum%
     1.85s   100%   100%      1.85s   100%  bench.Fib
         0     0%   100%      1.85s   100%  bench.BenchmarkFib10
         0     0%   100%      1.85s   100%  testing.(*B).launch
         0     0%   100%      1.85s   100%  testing.(*B).runN

這個是使用cpu 文件, 也可以使用內存文件

然後你也可以用list命令檢查函數需要的時間

(pprof) list Fib
     1.84s      2.75s (flat, cum) 148.65% of Total
         .          .      1:package bench
         .          .      2:
         .          .      3:import "testing"
         .          .      4:
     530ms      530ms      5:func Fib(n int) int {
     260ms      260ms      6:   if n < 2 {
     130ms      130ms      7:           return n
         .          .      8:   }
     920ms      1.83s      9:   return Fib(n-1) + Fib(n-2)
         .          .     10:}

或者使用web命令生成圖像(png,pdf,...)

1.7. Setup與TearDown

測試程序有時需要在測試之前進行額外的設置(setup)或在測試之後進行拆卸(teardown)。

1.7.1. TestMain

通過在*_test.go文件中定義TestMain函數來可以在測試之前進行額外的設置(setup)或在測試之後進行拆卸(teardown)操作。

如果測試文件包含函數:func TestMain(m *testing.M)那麼生成的測試會先調用 TestMain(m),然後再運行具體測試。

TestMain運行在主goroutine中, 可以在調用 m.Run前後做任何設置(setup)和拆卸(teardown)。

退出測試的時候應該使用m.Run的返回值作爲參數調用os.Exit。

一個使用TestMain來設置Setup和TearDown的示例如下:

func TestMain(m *testing.M) {
    fmt.Println("write setup code here...") // 測試之前的做一些設置
    // 如果 TestMain 使用了 flags,這裏應該加上flag.Parse()
    retCode := m.Run()                         // 執行測試
    fmt.Println("write teardown code here...") // 測試之後做一些拆卸工作
    os.Exit(retCode)                           // 退出測試
}

需要注意的是:在調用TestMain時, flag.Parse並沒有被調用。

所以如果TestMain 依賴於command-line標誌 (包括 testing 包的標記),則應該顯示的調用flag.Parse。

1.7.2. 子測試的Setup與Teardown

有時候我們可能需要爲每個測試集設置Setup與Teardown,也有可能需要爲每個子測試設置Setup與Teardown。

下面我們定義兩個函數工具函數如下:

// 測試集的Setup與Teardown
func setupTestCase(t *testing.T) func(t *testing.T) {
    t.Log("如有需要在此執行:測試之前的setup")
    return func(t *testing.T) {
        t.Log("如有需要在此執行:測試之後的teardown")
    }
}

// 子測試的Setup與Teardown
func setupSubTest(t *testing.T) func(t *testing.T) {
    t.Log("如有需要在此執行:子測試之前的setup")
    return func(t *testing.T) {
        t.Log("如有需要在此執行:子測試之後的teardown")
    }
}

使用方式如下:

func TestSplit(t *testing.T) {
    type test struct { // 定義test結構體
        input string
        sep   string
        want  []string
    }
    tests := map[string]test{ // 測試用例使用map存儲
        "simple":      {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
        "wrong sep":   {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
        "more sep":    {input: "abcd", sep: "bc", want: []string{"a", "d"}},
        "leading sep": {input: "枯藤老樹昏鴉", sep: "老", want: []string{"", "枯藤", "樹昏鴉"}},
    }
    teardownTestCase := setupTestCase(t) // 測試之前執行setup操作
    defer teardownTestCase(t)            // 測試之後執行testdoen操作

    for name, tc := range tests {
        t.Run(name, func(t *testing.T) { // 使用t.Run()執行子測試
            teardownSubTest := setupSubTest(t) // 子測試之前執行setup操作
            defer teardownSubTest(t)           // 測試之後執行testdoen操作
            got := Split(tc.input, tc.sep)
            if !reflect.DeepEqual(got, tc.want) {
                t.Errorf("excepted:%#v, got:%#v", tc.want, got)
            }
        })
    }
}

測試結果如下:

    split $ go test -v
    === RUN   TestSplit
    === RUN   TestSplit/simple
    === RUN   TestSplit/wrong_sep
    === RUN   TestSplit/more_sep
    === RUN   TestSplit/leading_sep
    --- PASS: TestSplit (0.00s)
        split_test.go:71: 如有需要在此執行:測試之前的setup
        --- PASS: TestSplit/simple (0.00s)
            split_test.go:79: 如有需要在此執行:子測試之前的setup
            split_test.go:81: 如有需要在此執行:子測試之後的teardown
        --- PASS: TestSplit/wrong_sep (0.00s)
            split_test.go:79: 如有需要在此執行:子測試之前的setup
            split_test.go:81: 如有需要在此執行:子測試之後的teardown
        --- PASS: TestSplit/more_sep (0.00s)
            split_test.go:79: 如有需要在此執行:子測試之前的setup
            split_test.go:81: 如有需要在此執行:子測試之後的teardown
        --- PASS: TestSplit/leading_sep (0.00s)
            split_test.go:79: 如有需要在此執行:子測試之前的setup
            split_test.go:81: 如有需要在此執行:子測試之後的teardown
        split_test.go:73: 如有需要在此執行:測試之後的teardown
    === RUN   ExampleSplit
    --- PASS: ExampleSplit (0.00s)
    PASS
    ok      github.com/Q1mi/studygo/code_demo/test_demo/split       0.006s

1.8. 示例函數 Example

1.8.1. 示例函數的格式

被go test特殊對待的第三種函數就是示例函數,它們的函數名以Example爲前綴。它們既沒有參數也沒有返回值。標準格式如下:

func ExampleName() {
    // ...
}

1.8.2. 示例函數示例

下面的代碼是我們爲Split函數編寫的一個示例函數:

func ExampleSplit() {
    fmt.Println(split.Split("a:b:c", ":"))
    fmt.Println(split.Split("枯藤老樹昏鴉", "老"))
    // Output:
    // [a b c]
    // [ 枯藤 樹昏鴉]
}

爲你的代碼編寫示例代碼有如下三個用處:

1. 示例函數能夠作爲文檔直接使用,例如基於web的godoc中能把示例函數與對應的函數或包相關聯

2. 示例函數只要包含了// Output:也是可以通過go test運行的可執行測試。

        split $ go test -run Example
        PASS
        ok      github.com/pprof/studygo/code_demo/test_demo/split       0.006s


3. 示例函數提供了可以直接運行的示例代碼,可以直接在golang.org的godoc文檔服務器上使用Go Playground運行示例代碼。
 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章