go test 單元測試

go test [flag] flag 有:

go help testflag 查看有那些 flag:

  • -args: 測試函數接收命令行參數,注意:該參數後所有參數會被當做命令行參數傳遞給測試用例,正確使用方式舉例:go test -v -run TestFibOnce b_test.go -args "in 7" expected=13
  • -c: 將測試文件編譯生成可執行函數, 加 -o 指定文件名(默認文件名test.test);
  • 編譯:go test -c -o test_fib.test b_test.go
  • 使用二進制文件:./test_fib.test -test.v -test.run TestFibOnce "in=7" expected=13 測試用例同上;T
  • -i: 安裝作爲測試依賴項的軟件包。不要運行測試。
  • -json: 以 json 格式輸出。
  • -bench regexp:僅運行與正則表達式匹配的那些基準。多個正則表達式以 / 隔開
  • -benchtime t: 對每個基準運行足夠的迭代,以 t 表示爲 time.Duration(例如 -benchtime 1h30s)。
  • 默認值爲1秒(1s)。
  • 特殊語法 Nx 意味着要運行基準測試N次(例如,-benchtime 100x)。
  • -count n: 運行每個測試和基準測試 n 次(默認爲1)。如果設置了-cpu,則對每個 GOMAXPROCS 值運行n次。示例始終運行一次。
  • -cover: 覆蓋率
  • -covermode set,count,atomic: 設置要測試的包裝的覆蓋率分析的模式。
  • -cpu: 指定應爲其執行測試或基準的GOMAXPROCS值的列表。 默認值爲GOMAXPROCS的當前值。
  • -failfast: 第一次測試失敗後,請勿開始新的測試。
  • -list regexp: 列出 與正則匹配的測試用例列表,例如 go test -list Test . 列出名字以 Test 爲開頭的測試用例;
  • -parallel n: 調用t.Parallel的測試功能。並設置 並行運行的測試數量爲n,n 默認爲 GOMAXPROCS,請注意,-parallel僅適用於單個測試二進制文件。
  • -run regexp: 執行與正則表達式相匹配的測試用例;
  • -short: 一個快速測試的標記,在測試用例中可以使用 testing.Short() 來繞開一些測試,詳細使用方法看 -short 的使用;
  • -timeout d: 如果測試用例的運行時間超過持續時間 d,則出現恐慌。如果 d 爲 0,則禁用超時。默認值爲 10分鐘(10m);
  • -v: 顯示詳細測試信息,打印 t.Log()t.Logf() 輸出;
  • -benchmem: 打印基準測試的內存分配統計信息。
  • -blockprofile block.out: 性能剖析, 記錄 阻塞事件的分析數據 到 block.out,可以供給go tool pprof 使用。
  • 例如:
    go tool pprof test.test block.out 輸入 web 會生成 svg 圖像(需要安裝 graphviz):
    在這裏插入圖片描述
  • -blockProfilerate n: 探查器每n納秒中採樣一個阻塞事件;
  • -coverprofile cover.out: 看覆蓋率
  • -cpuprofile cpu.out: 性能剖析, 記錄 cpu 性能刨析 到文件,可以供給 go tool pprof 使用
  • 例如:
    go tool pprof test.test cpu.out 輸入 web 會生成 svg 圖像(需要安裝 graphviz):
    在這裏插入圖片描述
  • -memprofile mem.out: 性能剖析, 同上 記錄內存使用數據到文件,可以供給 go tool pprof 使用
  • 例如:
    go tool pprof test.test mem.out 輸入 web 會生成 svg 圖像(需要安裝 graphviz):
    mem

打印/報告

  1. 當我們遇到一個斷言錯誤的時候,標識這個測試失敗,會使用到:
Fail: 測試失敗,測試繼續,也就是之後的代碼依然會執行
FailNow: 測試失敗,測試中斷

在 FailNow 方法實現的內部,是通過調用 runtime.Goexit() 來中斷測試的。

  1. 當我們遇到一個斷言錯誤,只希望跳過這個錯誤,但是不希望標識測試失敗,會使用到:
SkipNow: 跳過測試,測試中斷
在 SkipNow 方法實現的內部,是通過調用 `runtime.Goexit()` 來中斷測試的。
  1. 當我們只希望打印信息,會用到 :
Log: 輸出信息
Logf: 輸出格式化的信息

注意:默認情況下,單元測試成功時,它們打印的信息不會輸出,可以通過加上 -v` 選項,輸出這些信息。但對於基準測試,它們總是會被輸出。

  1. 當我們希望跳過這個測試,並且打印出信息,會用到:
Skip: 相當於 Log + SkipNow
Skipf: 相當於 Logf + SkipNow
  1. 當我們希望斷言失敗的時候,標識測試失敗,並打印出必要的信息,但是測試繼續,會用到:
Error: 相當於 Log + Fail
Errorf: 相當於 Logf + Fail
  1. 當我們希望斷言失敗的時候,標識測試失敗,打印出必要的信息,但中斷測試,會用到:
Fatal: 相當於 Log + FailNow
Fatalf: 相當於 Logf + FailNow

T 類型 普通測試用例

T 類型用於管理測試狀態並支持格式化測試日誌。測試日誌會在執行測試的過程中不斷累積,並在測試完成時轉儲至標準輸出。測試用例以 Test 開頭:

// 被測試的函數
func Fib(n int) int {
    if n < 2 {
            return n
    }
    return Fib(n-1) + Fib(n-2)
}
// 執行測試
func TestFib(t *testing.T) {
    var fibTests = []struct {
        in       int // input
        expected int // expected result
    }{
        {1, 1},
        {2, 1},
        {3, 2},
        {4, 3},
        {5, 5},
        {6, 8},
        {7, 13},
    }

    for _, tt := range fibTests {
        actual := Fib(tt.in)
        if actual != tt.expected {      // 斷言結果是否和預期相等
            t.Errorf("Fib(%d) = %d; expected %d", tt.in, actual, tt.expected)
        }
    }
}
/* go test -v -run TestFib b_test.go
# 參數說明: 
    -v  打印詳細信息
    -run 運行執行測試用例函數
    後跟用例尋找範圍,文件名 或 "."(代表當前目錄下所有文件)
# 輸出:
=== RUN   TestFib
--- PASS: TestFib (0.00s)
PASS
ok      command-line-arguments  0.002s
*/

除了打印報告的方法外還有:方法:

Name 返回當前測試用例名稱

func (t *T) Name() string

Parallel 標記當前測試用例可以並行測試

func (t *T) Parallel()

比如下面兩個測試用例可並行執行測試:

func TestOne(t *testing.T) {
    t.Parallel()
    ...
}
func TestTwo(t *testing.T) {
    t.Parallel()
    ...
}

Helper 將函數標記爲測試助手函數

func (t *T) Helper()

使用示例:

func failure(t *testing.T) {
    t.Helper()                 // 標記自己爲helper函數
    t.Fatal("failure")
}
func TestHelper(t *testing.T){
    failure(t)
}
/*   執行 o test -v -run TestHelper  輸出:
~/Projects/go/src/test/test $  go test -v -run TestHelper
=== RUN   TestHelper
    TestHelper: b_test.go:128: failure          // 這裏錯誤信息 顯示是在 第 128 行 即 TestHelper 函數中
--- FAIL: TestHelper (0.00s)
FAIL
exit status 1
FAIL    test/test       0.006s

對比註釋掉 t.Helper() 輸出: 
~/Projects/go/src/test/test $  go test -v -run TestHelper
=== RUN   TestHelper
    TestHelper: b_test.go:125: failure     // 這裏錯誤信息 顯示是在 第 128 行 即 failure 函數中
--- FAIL: TestHelper (0.00s)
FAIL
exit status 1
FAIL    test/test       0.006s
*/

Run 執行 子測試

func (t *T) Run(name string, f func(b *B)) bool

子測試,又叫 命名測試 (named tests),它意味着您現在可以擁有嵌套測試,這對於自定義(和過濾)給定測試的示例非常有用。
使用示例:

func TestFoo(t *testing.T) {
    // <setup code>
    t.Run("A=1", func(t *testing.T) { ... })
    t.Run("A=2", func(t *testing.T) { ... })
    t.Run("B=1", func(t *testing.T) { ... })
    // <tear-down code>
}

可通過指定 -run regexp-bench regexp flag 的正則來執行某子測試:

go test -run ''      # Run 所有測試。
go test -run Foo     # Run 匹配 "Foo" 的頂層測試,例如 "TestFooBar"。
go test -run Foo/A=  # 匹配頂層測試 "Foo",運行其匹配 "A=" 的子測試。
go test -run /A=1    # 運行所有匹配 "A=1" 的子測試。

子測試也可以使用 t.Parallel() 來標記並行執行。所有的子測試完成後,父測試纔會完成。在下面👇這個例子中,所有的測試是相互並行運行的,當然也只是彼此之間,不包括定義在其他頂層測試的子測試:

func TestGroupedParallel(t *testing.T) {
    for _, tc := range tests {
        tc := tc // capture range variable
        t.Run(tc.Name, func(t *testing.T) {
            t.Parallel()
            ...
        })
    }
}

B 類型 基準測試(壓力測試)

B 類型用於管理基準測試的計時行爲,並指示應該迭代地運行測試多少次。B 類型測試用例以 Benchmark 開頭:

func BenchmarkHello(b *testing.B) {
    for i := 0; i < b.N; i++ {
        fmt.Sprintf("hello")
    }
}

func BenchmarkBenchmem(b *testing.B) {
    for n := 0; n < b.N; n++ {
        actual := Fib(7)
        fmt.Printf("Fib(%d)=%d\n", n, actual)
    }
}

B 類型有 T 類型的所有方法,除了和 T 共有的函數外還有:

ReportAllocs 打開當前基準測試的內存統計功能

func (b *B) ReportAllocs()

打開當前基準測試的內存統計功能,與使用 -test.benchmem 設置類似,但 ReportAllocs 隻影響那些調用了該函數的基準測試。

ResetTimer 重置計時器

func (b *B) ResetTimer()

對已經逝去的基準測試時間以及內存分配計數器進行清零。對於正在運行中的計時器,這個方法不會產生任何效果。
使用示例:

func BenchmarkBigLen(b *testing.B) {
    big := NewBig()      // 費時操作 比如初始化某個變量
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        big.Len()
    }
}

RunParallel 並行執行給定測試

func (b *B) RunParallel(body func(*PB))

以並行的方式執行給定的基準測試。 RunParallel 會創建出多個 goroutine ,並將 b.N 分配給這些 goroutine 執行, 其中 goroutine 數量的默認值爲 GOMAXPROCS 。用戶如果想要增加非 CPU 受限(``non-CPU-bound)基準測試的並行性, 那麼可以在RunParallel之前調用SetParallelismRunParallel 通常會與-cpu` 標誌一同使用。

body 函數將在每個 goroutine 中執行,這個函數需要設置所有 goroutine 本地的狀態, 並迭代直到 pb.Next返回 false 值爲止。因爲 StartTimerStopTimerResetTimer 這三個函數都帶有全局作用,所以 body 函數不應該調用這些函數;除此之外,body 函數也不應該調用 Run 函數。

func BenchmarkRunParallel(b *testing.B) {
    templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
    // RunParallel 將創建 GOMAXPROCS 個 goroutine,並在其中分配工作。
    b.RunParallel(func(pb *testing.PB){
        // 每個goroutine都有自己的byte.Buffer。
        var buf bytes.Buffer
        for pb.Next() {
            // 循環體在所有goroutine中總共執行b.N次。
            buf.Reset()
            templ.Execute(&buf, "World")
        }
    })
}
// go test -v -bench=^BenchmarkRunParallel$ -run=^$

SetBytes 記錄處理的字節數

func (b *B) SetBytes(n int64)

記錄在單個操作中處理的字節數量。 在調用了這個方法之後, 基準測試將會報告 ns/op 以及 MB/s.

SetParallelism 開始對測試進行計時

func (b *B) StartTimer()

這個函數在基準測試開始時會自動被調用,它也可以在調用 StopTimer 之後恢復進行計時。

StopTimer 停止對測試進行計時。

func (b *B) StopTimer()

停止對測試進行計時。

測試控制檯輸出的例子

func ExampleHello() {
    fmt.Println("Hello")
    // Output: hello
}

/* go test -run ExampleHello
# 輸出:
--- FAIL: ExampleHello (0.00s)
got:
Hello
want:
hello
FAIL
FAIL    command-line-arguments    0.009s
FAIL

Main 測試

TestMain 的使用場景:
開始測試之前有初始化操作,比如 http 測試有時需要授權操作、創建連接時。
測試結束後要做數據清理等操作時。

func Add(a,b int) int {
    return a+b
}

func TestMain(m *testing.M) {
    fmt.Println("開始測試...")
    m.Run()
    fmt.Println("測試結束...")
}

/* 輸出:
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
測試結束...
ok      command-line-arguments  0.007s

HTTP 測試

Go 語言目前的 web 開發是比較多的,那麼在我們對功能函數有了測試之後,HTTP 的測試又該怎樣做呢?

Go 的標準庫爲我們提供了一個 httptest 的庫,通過它就能夠輕鬆的完成 HTTP 的測試。
示例1:

package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "net/http/httptest"
)

var HandleHelloWorld = func(w http.ResponseWriter, r *http.Request) {
    io.WriteString(w, "<html><body>Hello World!</body></html>")
}

func main() {
    req := httptest.NewRequest("GET", "http://example.com/foo", nil)
    w := httptest.NewRecorder()
    HandleHelloWorld(w, req)

    resp := w.Result()
    body, _ := ioutil.ReadAll(resp.Body)

    fmt.Println(resp.StatusCode)
    fmt.Println(resp.Header.Get("Content-Type"))
    fmt.Println(string(body))
}

示例2:

package test

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "testing"
)

func testAPI(w http.ResponseWriter, r *http.Request){
    body, err := ioutil.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "請求 body 異常", 500)
    }
    fmt.Println(string(body))
    // fmt.Fprint(w, "ok")
    http.Error(w, "請求 body 異常", 500)
}


func Test_testApi(t *testing.T) {
    tests := []struct {
        name string
    }{
        {
            name: "test api",
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T){
            // 傳入一個 http 處理器 創建一個 server 
            ts := httptest.NewServer(http.HandlerFunc(testAPI))
            defer ts.Close()

            params := struct{
                Params string    `json:"params"`
            }{
                Params: "params body",
            }
            paramsByte, _ := json.Marshal(params)

            // 像上面那個處理器發送一個 post 請求
            resp, err := http.Post(ts.URL, "application/json", bytes.NewBuffer(paramsByte))
            if err != nil {
                t.Error(err)
            }
            defer resp.Body.Close()
            
            // 檢查返回 http status 
            t.Logf("Status Code: %d",resp.StatusCode)
            if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
                body, _ := ioutil.ReadAll(resp.Body)
                t.Error(string(body))
            }
        })
    }
}
/*
=== RUN   Test_testApi
=== RUN   Test_testApi/test_api
{"params":"params body"}
    Test_testApi/test_api: http_test.go:51: Status Code: 500
    Test_testApi/test_api: http_test.go:54: 請求 body 異常
        
--- FAIL: Test_testApi (0.00s)
    --- FAIL: Test_testApi/test_api (0.00s)
FAIL
FAIL    command-line-arguments  0.021s
FAIL 
*/

示例3 beego 框架測試:

b, err := json.Marshal(&Req{Username:"test", Passoword:"123456"})
r, _ := http.NewRequest("POST", "/user/login", bytes.NewBuffer(b))
r.Header.Set("User-Agent", "beego_server")
r.Body = ioutil.NopCloser(bytes.NewBuffer(b))
r.ContentLength = int64(len(b))
r.Header.Set("Content-Type", "application/json")

w := httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)

更復雜的示例:beego 單元測試示例.md

補充

覆蓋率

由單元測試的代碼,觸發運行到的被測試代碼的代碼行數佔所有代碼行數的比例,被稱爲測試覆蓋率,代碼覆蓋率不一定完全精準,但是可以作爲參考,可以幫我們測量和我們預計的覆蓋率之間的差距。
go tool 工具提供了測量覆蓋率的功能:

# 生成指定 package 的測試覆蓋率(fib.out 後面不帶參數的,默認是命令所在目錄)
go test -v -covermode=count -coverprofile fib.out
# 查看彙總的 fib 測試覆蓋率
go tool cover -func=fib.out
# 生成 html
go tool cover -html=fib.out -o fib.html

使用Short標記可跳過的測試用例

func TestTimeConsuming(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping test in short mode.")
    }
    ...
}

運行 go test 時添加 -short flag 即可跳過如上面代碼的測試用例,go test -short -v -run=.

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