摘要:功能測試函數功能測試函數需要接收類型的單一參數,類型用來管理測試狀態和支持格式化的測試日志。測試函數的相關說明,可以通過來查看幫助文檔。下面是一個例子被測試的進程退出函數測試進程退出函數的測試函數參考資料原文鏈接
原文鏈接:http://tabalt.net/blog/golang...
Golang作為一門標榜工程化的語言,提供了非常簡便、實用的編寫單元測試的能力。本文通過Golang源碼包中的用法,來學習在實際項目中如何編寫可測試的Go代碼。
第一個測試 “Hello Test!”首先,在我們$GOPATH/src目錄下創建hello目錄,作為本文涉及到的所有示例代碼的根目錄。
然后,新建名為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的文件,填入如下內容:
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開頭的說明被測試函數用法的函數。
大部分情況下,測試代碼是作為某個包的一部分,意味著它可以訪問包中不可導出的元素。但在有需要的時候(如避免循環依賴)也可以修改測試文件的包名,如package hello的測試文件,包名可以設為package hello_test。
功能測試函數需要接收*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 " 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來查看幫助文檔。
高級測試技術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)
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...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/8738.html
摘要:我們已經準備好經歷一段痛苦的撰寫單元測試的過程了,但最終我們能夠撰寫可測試的。這種代碼是很容易進行集成測試的,但幾乎不可能針對功能單元進行單獨的測試。我們絕對可以寫出集成測試的代碼,但我們應該很難寫出單元測試了。 轉自 勾三股四 - 撰寫可測試的 JavaScript 這篇文章算是 A List Apart 系列文章中,包括滑動門在內,令我印象最深刻的文章之一。最近有時間翻譯了一下,...
摘要:而測試驅動開發技術并不只是單純的測試工作。需求向來就是軟件開發過程中感覺最不好明確描述易變的東西。這里說的需求不只是指用戶的需求,還包括對代碼 可能很多人和我一樣, 首次聽到前端架構這個詞, 第一反應是: 前端還有架構這一說呢? 在后端開發領域, 系統規劃和可擴展性非常關鍵, 因此架構師備受重視, 早在開發工作啟動之前, 他們就被邀請加入到項目中, 而且他們會跟客戶討論即將建成的平臺的...
摘要:而測試驅動開發技術并不只是單純的測試工作。需求向來就是軟件開發過程中感覺最不好明確描述易變的東西。這里說的需求不只是指用戶的需求,還包括對代碼 可能很多人和我一樣, 首次聽到前端架構這個詞, 第一反應是: 前端還有架構這一說呢? 在后端開發領域, 系統規劃和可擴展性非常關鍵, 因此架構師備受重視, 早在開發工作啟動之前, 他們就被邀請加入到項目中, 而且他們會跟客戶討論即將建成的平臺的...
摘要:預覽因為項目是基于做的,本身就提供了的預覽組件,使用起來也簡單,如果業務需求需要放大縮小,這個組件就不滿足了。 需求分析 業務要求,需要一個圖片上傳控件,需滿足 多圖上傳 點擊預覽 圖片前端壓縮 支持初始化數據 相關功能及資源分析 基本功能先到https://www.npmjs.com/search?q=vue+upload上搜索有關上傳的控件,沒有完全滿足需求的組件,過濾后找到...
閱讀 986·2021-09-26 10:15
閱讀 2064·2021-09-24 10:37
閱讀 2580·2019-08-30 13:46
閱讀 2631·2019-08-30 11:16
閱讀 2421·2019-08-29 10:56
閱讀 2591·2019-08-26 12:24
閱讀 3473·2019-08-23 18:26
閱讀 2662·2019-08-23 15:43