編寫可測試的Go代碼

原文連接:http://tabalt.net/blog/golang...git

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

第一個測試 「Hello Test!」

首先,在咱們$GOPATH/src目錄下建立hello目錄,做爲本文涉及到的全部示例代碼的根目錄。golang

而後,新建名爲hello.go的文件,定義一個函數hello(),功能是返回一個由若干單詞拼接成句子:正則表達式

package hello

func 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的文件,填入以下內容:shell

package hello

import (
    "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命令並回車,能夠看到以下輸出:併發

PASS
ok      hello  0.007s

編寫測試代碼

Golang的測試代碼位於某個包的源代碼中名稱以_test.go結尾的源文件裏,測試代碼包含測試函數、測試輔助代碼和示例函數;測試函數有以Test開頭的功能測試函數和以Benchmark開頭的性能測試函數兩種,測試輔助代碼是爲測試函數服務的公共函數、初始化函數、測試數據等,示例函數則是以Example開頭的說明被測試函數用法的函數。app

大部分狀況下,測試代碼是做爲某個包的一部分,意味着它能夠訪問包中不可導出的元素。但在有須要的時候(如避免循環依賴)也能夠修改測試文件的包名,如package hello的測試文件,包名能夠設爲package hello_testide

功能測試函數

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

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

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

# /usr/local/go/src/bytes/compare_test.go
func 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.go
func 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.go
func 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.go
func 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.go
func 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.go
func 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.go
func 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.go
func 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.go
func 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.go
func 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)
PASS
ok      hello  0.006s

假設咱們有不少功能測試函數,但某次測試只想執行其中的某一些,能夠經過-run參數,使用正則表達式來匹配要執行的功能測試函數名。以下面指定參數後,功能測試函數TestHello不會執行到。

[tabalt@localhost hello] go test -v -run=xxx
PASS
ok      hello  0.006s

性能測試函數默認並不會執行,須要添加-bench參數,並指定匹配性能測試函數名的正則表達式;例如,想要執行某個包中全部的性能測試函數能夠添加參數-bench .-bench=.

[tabalt@localhost hello] go test -bench=.
PASS
BenchmarkHello-8     2000000           657 ns/op
ok      hello  1.993s

想要查看性能測試時的內存狀況,能夠再添加參數-benchmem

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

參數-cover能夠用來查看咱們編寫的測試對代碼的覆蓋率:

[tabalt@localhost hello] go test -cover
PASS
coverage: 100.0% of statements
ok      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.go
func 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...

相關文章
相關標籤/搜索