編寫可測試的Go代碼

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

原文連結:http://tabalt.net/blog/golang...

Golang作為一門標榜工程化的語言,提供了非常簡便、實用的編寫單元測試的能力。本文通過Golang源碼包中的用法,來學習在實際項目中如何編寫可測試的Go代碼。

第一個測試 “Hello Test!”

首先,在我們$GOPATH/src目錄下建立hello目錄,作為本文涉及到的所有範例程式碼的根目錄。

然後,建立名為hello.go的檔案,定義一個函數hello(),功能是返回一個由若干單詞拼接成句子:

package hellofunc hello() string {    words := []string{"hello", "func", "in", "package", "hello"}    wl := len(words)    sentence := ""    for key, word := range words {        sentence += word        if key < wl-1 {            sentence += " "        } else {            sentence += "."        }    }    return sentence}

接著,建立名為hello_test.go的檔案,填入如下內容:

package helloimport (    "fmt"    "testing")func TestHello(t *testing.T) {    got := hello()    expect := "hello func in package hello."    if got != expect {        t.Errorf("got [%s] expected [%s]", got, expect)    }}func BenchmarkHello(b *testing.B) {    for i := 0; i < b.N; i++ {        hello()    }}func ExampleHello() {    hl := hello()    fmt.Println(hl)    // Output: hello func in package hello.}

最後,開啟終端,進入hello目錄,輸入go test命令並斷行符號,可以看到如下輸出:

PASSok      hello  0.007s

編寫測試代碼

Golang的測試代碼位於某個包的原始碼中名稱以_test.go結尾的源檔案裡,測試程式碼封裝含測試函數、測試輔助代碼和樣本函數;測試函數有以Test開頭的功能測試函數和以Benchmark開頭的效能測試函數兩種,測試輔助代碼是為測試函數服務的公用函數、初始化函數、測試資料等,樣本函數則是以Example開頭的說明被測試函數用法的函數。

大部分情況下,測試代碼是作為某個包的一部分,意味著它可以訪問包中不可匯出的元素。但在有需要的時候(如避免循環相依性)也可以修改測試檔案的包名,如package hello的測試檔案,包名可以設為package hello_test

功能測試函數

功能測試函數需要接收*testing.T類型的單一參數t,testing.T 類型用來管理測試狀態和支援格式化的測試日誌。測試日誌在測試執行過程中積累起來,完成後輸出到標準錯誤輸出。

下面是從Go標準庫摘抄的 testing.T類型的常用方法的用法:

  • 測試函數中的某條測試案例執行結果與預期不符時,調用t.Error()或t.Errorf()方法記錄日誌並標記測試失敗

# /usr/local/go/src/bytes/compare_test.gofunc TestCompareIdenticalSlice(t *testing.T) {    var b = []byte("Hello Gophers!")    if Compare(b, b) != 0 {        t.Error("b != b")    }    if Compare(b, b[:1]) != 1 {        t.Error("b > b[:1] failed")    }}
  • 使用t.Fatal()和t.Fatalf()方法,在某條測試案例失敗後就跳出該測試函數

# /usr/local/go/src/bytes/reader_test.gofunc TestReadAfterBigSeek(t *testing.T) {    r := NewReader([]byte("0123456789"))    if _, err := r.Seek(1<<31+5, os.SEEK_SET); err != nil {        t.Fatal(err)    }    if n, err := r.Read(make([]byte, 10)); n != 0 || err != io.EOF {        t.Errorf("Read = %d, %v; want 0, EOF", n, err)    }}
  • 使用t.Skip()和t.Skipf()方法,跳過某條測試案例的執行

# /usr/local/go/src/archive/zip/zip_test.gofunc TestZip64(t *testing.T) {    if testing.Short() {        t.Skip("slow test; skipping")    }    const size = 1 << 32 // before the "END\n" part    buf := testZip64(t, size)    testZip64DirectoryRecordLength(buf, t)}
  • 執行測試案例的過程中通過t.Log()和t.Logf()記錄日誌

# /usr/local/go/src/regexp/exec_test.gofunc TestFowler(t *testing.T) {    files, err := filepath.Glob("testdata/*.dat")    if err != nil {        t.Fatal(err)    }    for _, file := range files {        t.Log(file)        testFowler(t, file)    }}
  • 使用t.Parallel()標記需要並發執行的測試函數

# /usr/local/go/src/runtime/stack_test.gofunc TestStackGrowth(t *testing.T) {    t.Parallel()    var wg sync.WaitGroup    // in a normal goroutine    wg.Add(1)    go func() {        defer wg.Done()        growStack()    }()    wg.Wait()    // ...}

效能測試函數

效能測試函數需要接收*testing.B類型的單一參數b,效能測試函數中需要迴圈b.N次調用被測函數。testing.B 類型用來管理測試時間和迭代運行次數,也支援和testing.T相同的方式管理測試狀態和格式化的測試日誌,不一樣的是testing.B的日誌總是會輸出。

下面是從Go標準庫摘抄的 testing.B類型的常用方法的用法:

  • 在函數中調用t.ReportAllocs(),啟用記憶體使用量分析

# /usr/local/go/src/bufio/bufio_test.gofunc BenchmarkWriterFlush(b *testing.B) {    b.ReportAllocs()    bw := NewWriter(ioutil.Discard)    str := strings.Repeat("x", 50)    for i := 0; i < b.N; i++ {        bw.WriteString(str)        bw.Flush()    }}
  • 通過 b.StopTimer()、b.ResetTimer()、b.StartTimer()來停止、重設、啟動 時間經過和記憶體配置計數

# /usr/local/go/src/fmt/scan_test.gofunc BenchmarkScanInts(b *testing.B) {    b.ResetTimer()    ints := makeInts(intCount)    var r RecursiveInt    for i := b.N - 1; i >= 0; i-- {        buf := bytes.NewBuffer(ints)        b.StartTimer()        scanInts(&r, buf)        b.StopTimer()    }}
  • 調用b.SetBytes()記錄在一個操作中處理的位元組數

# /usr/local/go/src/testing/benchmark.gofunc BenchmarkFields(b *testing.B) {    b.SetBytes(int64(len(fieldsInput)))    for i := 0; i < b.N; i++ {        Fields(fieldsInput)    }}
  • 通過b.RunParallel()方法和 *testing.PB類型的Next()方法來並發執行被測對象

# /usr/local/go/src/sync/atomic/value_test.gofunc BenchmarkValueRead(b *testing.B) {    var v Value    v.Store(new(int))    b.RunParallel(func(pb *testing.PB) {        for pb.Next() {            x := v.Load().(*int)            if *x != 0 {                b.Fatalf("wrong value: got %v, want 0", *x)            }        }    })}

測試輔助代碼

測試輔助代碼是編寫測試代碼過程中因代碼重用和代碼品質考慮而產生的。主要包括如下方面:

  • 引入依賴的外部包,如每個測試檔案都需要的 testing 包等:

# /usr/local/go/src/log/log_test.go:import (    "bytes"    "fmt"    "os"    "regexp"    "strings"    "testing"    "time")
  • 定義多次用到的常量和變數,測試案例資料等:

# /usr/local/go/src/log/log_test.go:const (    Rdate         = `[0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9]`    Rtime         = `[0-9][0-9]:[0-9][0-9]:[0-9][0-9]`    Rmicroseconds = `\.[0-9][0-9][0-9][0-9][0-9][0-9]`    Rline         = `(57|59):` // must update if the calls to l.Printf / l.Print below move    Rlongfile     = `.*/[A-Za-z0-9_\-]+\.go:` + Rline    Rshortfile    = `[A-Za-z0-9_\-]+\.go:` + Rline)// ...var tests = []tester{    // individual pieces:    {0, "", ""},    {0, "XXX", "XXX"},    {Ldate, "", Rdate + " "},    {Ltime, "", Rtime + " "},    {Ltime | Lmicroseconds, "", Rtime + Rmicroseconds + " "},    {Lmicroseconds, "", Rtime + Rmicroseconds + " "}, // microsec implies time    {Llongfile, "", Rlongfile + " "},    {Lshortfile, "", Rshortfile + " "},    {Llongfile | Lshortfile, "", Rshortfile + " "}, // shortfile overrides longfile    // everything at once:    {Ldate | Ltime | Lmicroseconds | Llongfile, "XXX", "XXX" + Rdate + " " + Rtime + Rmicroseconds + " " + Rlongfile + " "},    {Ldate | Ltime | Lmicroseconds | Lshortfile, "XXX", "XXX" + Rdate + " " + Rtime + Rmicroseconds + " " + Rshortfile + " "},}
  • 和普通的Golang原始碼一樣,測試代碼中也能定義init函數,init函數會在引入外部包、定義常量、聲明變數之後被自動調用,可以在init函數裡編寫測試相關的初始化代碼。

# /usr/local/go/src/bytes/buffer_test.gofunc init() {    testBytes = make([]byte, N)    for i := 0; i < N; i++ {        testBytes[i] = 'a' + byte(i%26)    }    data = string(testBytes)}
  • 封裝測試專用的公用函數,抽象測試專用的結構體等:

# /usr/local/go/src/log/log_test.go:type tester struct {    flag    int    prefix  string    pattern string // regexp that log output must match; we add ^ and expected_text$ always}// ...func testPrint(t *testing.T, flag int, prefix string, pattern string, useFormat bool) {    // ...}

樣本函數

樣本函數無需接收參數,但需要使用注釋的 Output: 標記說明樣本函數的輸出值,未指定Output:標記或輸出值為空白的樣本函數不會被執行。

樣本函數需要歸屬於某個 包/函數/類型/類型 的方法,具體命名規則如下:

func Example() { ... }      # 包的樣本函數func ExampleF() { ... }     # 函數F的樣本函數func ExampleT() { ... }     # 類型T的樣本函數func ExampleT_M() { ... }   # 類型T的M方法的樣本函數# 多樣本函數 需要跟底線加小寫字母開頭的尾碼func Example_suffix() { ... }func ExampleF_suffix() { ... }func ExampleT_suffix() { ... }func ExampleT_M_suffix() { ... }

go doc 工具會解析樣本函數的函數體作為對應 包/函數/類型/類型的方法 的用法。

測試函數的相關說明,可以通過go help testfunc來查看協助文檔。

使用 go test 工具

Golang中通過命令列工具go test來執行測試代碼,開啟shell終端,進入需要測試的包所在的目錄執行 go test,或者直接執行go test $pkg_name_in_gopath即可對指定的包執行測試。

通過形如go test github.com/tabalt/...的命令可以執行$GOPATH/github.com/tabalt/目錄下所有的項目的測試。go test std命令則可以執行Golang標準庫的所有測試。

如果想查看執行了哪些測試函數及函數的執行結果,可以使用-v參數:

[tabalt@localhost hello] go test -v=== RUN   TestHello--- PASS: TestHello (0.00s)=== RUN   ExampleHello--- PASS: ExampleHello (0.00s)PASSok      hello  0.006s

假設我們有很多功能測試函數,但某次測試只想執行其中的某一些,可以通過-run參數,使用Regex來匹配要執行的功能測試函數名。如下面指定參數後,功能測試函數TestHello不會執行到。

[tabalt@localhost hello] go test -v -run=xxxPASSok      hello  0.006s

效能測試函數預設並不會執行,需要添加-bench參數,並指定匹配效能測試函數名的Regex;例如,想要執行某個包中所有的效能測試函數可以添加參數-bench .-bench=.

[tabalt@localhost hello] go test -bench=.PASSBenchmarkHello-8     2000000           657 ns/opok      hello  1.993s

想要查看效能測試時的記憶體情況,可以再添加參數-benchmem

[tabalt@localhost hello] go test -bench=. -benchmemPASSBenchmarkHello-8     2000000           666 ns/op         208 B/op          9 allocs/opok      hello  2.014s

參數-cover可以用來查看我們編寫的測試對代碼的覆蓋率:

[tabalt@localhost hello] go test -coverPASScoverage: 100.0% of statementsok      hello  0.006s

詳細的覆蓋率資訊,可以通過-coverprofile輸出到檔案,並使用go tool cover來查看,用法請參考go tool cover -help

更多go test命令的參數及用法,可以通過go help testflag來查看協助文檔。

進階測試技術

IO相關測試

testing/iotest包中實現了常用的出錯的Reader和Writer,可供我們在io相關的測試中使用。主要有:

  • 觸發資料錯誤dataErrReader,通過DataErrReader()函數建立

  • 讀取一半內容的halfReader,通過HalfReader()函數建立

  • 讀取一個byte的oneByteReader,通過OneByteReader()函數建立

  • 觸發逾時錯誤的timeoutReader,通過TimeoutReader()函數建立

  • 寫入指定位元內容後停止的truncateWriter,通過TruncateWriter()函數建立

  • 讀取時記錄日誌的readLogger,通過NewReadLogger()函數建立

  • 寫入時記錄日誌的writeLogger,通過NewWriteLogger()函數建立

黑箱測試

testing/quick包實現了協助黑箱測試的實用函數 Check和CheckEqual。

Check函數的第1個參數是要測試的只返回bool值的黑盒函數f,Check會為f的每個參數設定任意值並多次調用,如果f返回false,Check函數會返回錯誤值 *CheckError。Check函數的第2個參數 可以指定一個quick.Config類型的config,傳nil則會預設使用quick.defaultConfig。quick.Config結構體包含了測試回合的選項。

# /usr/local/go/src/math/big/int_test.gofunc checkMul(a, b []byte) bool {    var x, y, z1 Int    x.SetBytes(a)    y.SetBytes(b)    z1.Mul(&x, &y)    var z2 Int    z2.SetBytes(mulBytes(a, b))    return z1.Cmp(&z2) == 0}func TestMul(t *testing.T) {    if err := quick.Check(checkMul, nil); err != nil {        t.Error(err)    }}

CheckEqual函數是比較給定的兩個黑盒函數是否相等,函數原型如下:

func CheckEqual(f, g interface{}, config *Config) (err error)

HTTP測試

net/http/httptest包提供了HTTP相關代碼的工具,我們的測試代碼中可以建立一個臨時的httptest.Server來測試發送HTTP請求的代碼:

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {    fmt.Fprintln(w, "Hello, client")}))defer ts.Close()res, err := http.Get(ts.URL)if err != nil {    log.Fatal(err)}greeting, err := ioutil.ReadAll(res.Body)res.Body.Close()if err != nil {    log.Fatal(err)}fmt.Printf("%s", greeting)

還可以建立一個應答的記錄器httptest.ResponseRecorder來檢測應答的內容:

handler := func(w http.ResponseWriter, r *http.Request) {    http.Error(w, "something failed", http.StatusInternalServerError)}req, err := http.NewRequest("GET", "http://example.com/foo", nil)if err != nil {    log.Fatal(err)}w := httptest.NewRecorder()handler(w, req)fmt.Printf("%d - %s", w.Code, w.Body.String())

測試進程操作行為

當我們被測函數有操作進程的行為,可以將被測程式作為一個子進程執行測試。下面是一個例子:

//被測試的進程退出函數func Crasher() {    fmt.Println("Going down in flames!")    os.Exit(1)}//測試進程退出函數的測試函數func TestCrasher(t *testing.T) {    if os.Getenv("BE_CRASHER") == "1" {        Crasher()        return    }    cmd := exec.Command(os.Args[0], "-test.run=TestCrasher")    cmd.Env = append(os.Environ(), "BE_CRASHER=1")    err := cmd.Run()    if e, ok := err.(*exec.ExitError); ok && !e.Success() {        return    }    t.Fatalf("process ran with err %v, want exit status 1", err)}

參考資料

https://talks.golang.org/2014...
https://golang.org/pkg/testing/
https://golang.org/pkg/testin...
https://golang.org/pkg/testin...
https://golang.org/pkg/net/ht...

原文連結:http://tabalt.net/blog/golang...

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.