Golang 語言的單元測試和性能測試(也叫 壓力測試)
Golang單元測試對文件名和方法名,參數都有很嚴格的要求。
例如:
1、文件名必須以xx_test.go命名
2、方法必須是Test[^a-z]開頭(T必須大寫),func TestXxx (t *testing.T),Xxx部分可以為任意的字母數字的組合,但是首字母不能是小寫字母[a-z],例如Testintdiv是錯誤的函數名。
3、方法參數必須 t *testing.T
4、測試用例會按照源代碼中寫的順序依次執行
5、函數中通過調用testing.T的Error, Errorf, FailNow, Fatal, FatalIf方法,說明測試不通過,調用Log方法用來記錄測試的信息。
test的運行方式:
其中后面的test為test目錄
Go 提供了 TestMain(*testing.M) 的函數,它在需要的時候代替運行所有的測試。使用 TestMain() 函數時,您有機會在測試運行之前或之后插入所需的任何自定義代碼,但唯一需要注意的是必須處理 flag 解析并使用測試結果調用 os.Exit()。這聽起來可能很復雜,但實際上只有兩行代碼。
測試的覆蓋率
go tool命令可以報告測試覆蓋率統計。使用 go test -cover 測試覆蓋率。
可視化查看覆蓋率:
執行go tool cover -html=cover.out命令,會在/tmp目錄下生成目錄coverxxxxxxx,比如/tmp/cover404256298。目錄下有一個 coverage.html文件。用瀏覽器打開coverage.html,即可以可視化的查看代碼的測試覆蓋情況。
從內部測試
golang中大多數測試代碼都是被測試包的源碼的一部分。這意味著測試代碼可以訪問包種未導出的符號以及內部邏輯。就像我們之前看到的那樣。
注:比如$GOROOT/src/pkg/path/path_test.go與path.go都在path這個包下。
從外部測試
有些時候,你需要從被測包的外部對被測包進行測試,比如測試代碼在package foo_test下,而不是在package foo下。這樣可以打破依賴循環。
1、功能測試
上一個完整的例子:
// how to run: go test -v github.com/welhzh/dago/test package client import ( "flag" "fmt" "os" "testing" "github.com/welhzh/dago/client" ) func initTest() { } func destroyTest() { }
// test example func TestSum(t *testing.T) { // 任選一種測試方式進行測試 // ================= test 方式一 ==================== var testInputs = []struct { slice []int // other inputs expect int }{ {[]int{1, 2, 3, 4, 5}, 15}, {[]int{1, 2, 3, 4, -5}, 5}, } for _, oneInput := range testInputs { actual := oneInput.slice[2] if actual != oneInput.expect { t.Errorf("Sum(%q, %q) = %v; want %v", oneInput, oneInput, actual, oneInput.expect) } } // ================================================= // ================= test 方式二 ==================== t.Run("[1,2,3,4,5]", testSumFunc(testInputs[0].slice, testInputs[0].expect)) t.Run("[1,2,3,4,-5]", testSumFunc(testInputs[1].slice, testInputs[1].expect)) // ================================================= } func testSumFunc(numbers []int, expected int) func(*testing.T) { return func(t *testing.T) { actual := numbers[4] if actual != expected { t.Error(fmt.Sprintf("Expected the sum of %v to be %d but instead got %d!", numbers, expected, actual)) } } } func TestMain(m *testing.M) { fmt.Println("test begin ...") initTest() flag.Parse() exitCode := m.Run() destroyTest() fmt.Println("test end ...") // Exit os.Exit(exitCode) }
格式形如:
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 help testflag,這些是go test過程中經常使用到的參數
-test.v : 是否輸出全部的單元測試用例(不管成功或者失?。J沒有加上,所以只輸出失敗的單元測試用例。
-test.run pattern: 只跑哪些單元測試用例
-test.bench patten: 只跑那些性能測試用例
-test.benchmem : 是否在性能測試的時候輸出內存情況
-test.benchtime t : 性能測試運行的時間,默認是1s
-test.cpuprofile cpu.out : 是否輸出cpu性能分析文件
-test.memprofile mem.out : 是否輸出內存性能分析文件
-test.blockprofile block.out : 是否輸出內部goroutine阻塞的性能分析文件
-test.memprofilerate n : 內存性能分析的時候有一個分配了多少的時候才打點記錄的問題。這個參數就是設置打點的內存分配間隔,也就是profile中一個sample代表的內存大小。默認是設置為512 * 1024的。如果你將它設置為1,則每分配一個內存塊就會在profile中有個打點,那么生成的profile的sample就會非常多。如果你設置為0,那就是不做打點了。
你可以通過設置memprofilerate=1和GOGC=off來關閉內存回收,并且對每個內存塊的分配進行觀察。
-test.blockprofilerate n: 基本同上,控制的是goroutine阻塞時候打點的納秒數。默認不設置就相當于-test.blockprofilerate=1,每一納秒都打點記錄一下
-test.parallel n : 性能測試的程序并行cpu數,默認等于GOMAXPROCS。
-test.timeout t : 如果測試用例運行時間超過t,則拋出panic
-test.cpu 1,2,4 : 程序運行在哪些CPU上面,使用二進制的1所在位代表,和nginx的nginx_worker_cpu_affinity是一個道理
-test.short : 將那些運行時間較長的測試用例運行時間縮短
2、性能測試
如果需要進行性能測試,則函數開頭使用Benchmark就可以了。
//性能測試 func BenchmarkFibonacci(b *testing.B) { for i := 0; i < b.N; i++ { Fibonacci(10) } }
接下來執行這個性能測試:
$ go test -bench=. lib PASS BenchmarkFibonacci 5000000 436 ns/op ok lib 2.608s
其中第二行輸出表示這個函數運行了5000000次,平均運行一次的時間是436ns。
這個性能測試只測試參數為10的情況。如果有需要可以測試多個參數:
//測試參數為5的性能 func BenchmarkFibonacci5(b *testing.B) { for i := 0; i < b.N; i++ { Fibonacci(5) } } //測試參數為20的性能 func BenchmarkFibonacci20(b *testing.B) { for i := 0; i < b.N; i++ { Fibonacci(20) } }
運行一下:
$ go test -bench=. lib PASS BenchmarkFibonacci 5000000 357 ns/op BenchmarkFibonacci5 100000000 29.5 ns/op BenchmarkFibonacci20 50000 44688 ns/op ok lib 7.824s
如果性能測試的方法非常多,那需要的時間就會比較久。可以通過-bench=參數設置需要運行的性能測試的函數:
$ go test -bench=Fibonacci20 lib PASS BenchmarkFibonacci20 50000 44367 ns/op ok lib 2.677s
3、IO相關測試 (高級測試技術)
testing/iotest包中實現了常用的出錯的Reader和Writer,可供我們在io相關的測試中使用。主要有:
觸發數據錯誤dataErrReader,通過DataErrReader()函數創建
讀取一半內容的halfReader,通過HalfReader()函數創建
讀取一個byte的oneByteReader,通過OneByteReader()函數創建
觸發超時錯誤的timeoutReader,通過TimeoutReader()函數創建
寫入指定位數內容后停止的truncateWriter,通過TruncateWriter()函數創建
讀取時記錄日志的readLogger,通過NewReadLogger()函數創建
寫入時記錄日志的writeLogger,通過NewWriteLogger()函數創建
4、黑盒測試 (高級測試技術)
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)
5、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())
6、在進程里測試 (高級測試技術)
當我們被測函數有操作進程的行為,可以將被測程序作為一個子進程執行測試。下面是一個例子:
//被測試的進程退出函數 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(
7、競爭檢測 (高級測試技術)
當兩個goroutine并發訪問同一個變量,且至少一個goroutine對變量進行寫操作時,就會發生數據競爭(data race)。
為了協助診斷這種bug,Go提供了一個內置的數據競爭檢測工具。
通過傳入-race選項,go tool就可以啟動競爭檢測。
$ go test -race mypkg // to test the package
$ go run -race mysrc.go // to run the source file
$ go build -race mycmd // to build the command
$ go install -race mypkg // to install the package
一個數據競爭檢測的例子:
//testrace.go package main import “fmt” import “time” func main() { var i int = 0 go func() { for { i++ fmt.Println("subroutine: i = ", i) time.Sleep(1 * time.Second) } }() for { i++ fmt.Println("mainroutine: i = ", i) time.Sleep(1 * time.Second) } }
運行:
$ go run -race testrace.go
8、并發測試 (高級測試技術)testing with concurrency
當測試并發代碼時,總會有一種使用sleep的沖動。大多時間里,使用sleep既簡單又有效。
但大多數時間不是”總是“。
我們可以使用Go的并發原語讓那些奇怪不靠譜的sleep驅動的測試更加值得信賴。
9、靜態分析工具vet查找錯誤 (高級測試技術)
vet工具用于檢測代碼中程序員犯的常見錯誤:
– 錯誤的printf格式
– 錯誤的構建tag
– 在閉包中使用錯誤的range循環變量
– 無用的賦值操作
– 無法到達的代碼
– 錯誤使用mutex
等等。
使用方法:
go vet [package]
10、Mocks和fakes (高級測試技術)
通過在代碼中使用interface,Go可以避免使用mock和fake測試機制。
例如,如果你正在編寫一個文件格式解析器,不要這樣設計函數:
func Parser(f *os.File) error
作為替代,你可以編寫一個接受interface類型的函數:
func Parser(r io.Reader) error
和bytes.Buffer、strings.Reader一樣,*os.File也實現了io.Reader接口。
支付寶掃一掃捐贈
微信公眾號: 共鳴圈
歡迎討論,郵件: 924948$qq.com 請把$改成@
QQ群:263132197
QQ: 924948
浙公網安備 33010602011771號