Go語(yǔ)言單元測(cè)試的實(shí)現(xiàn)及用例
1.go test工具
Go語(yǔ)言中的測(cè)試依賴(lài)go test
命令。編寫(xiě)測(cè)試代碼和編寫(xiě)普通的Go代碼過(guò)程是類(lèi)似的,并不需要學(xué)習(xí)新的語(yǔ)法、規(guī)則或工具。
go test命令是一個(gè)按照一定約定和組織的測(cè)試代碼的驅(qū)動(dòng)程序。在包目錄內(nèi),所有以_test.go
為后綴名的源代碼文件都是go test
測(cè)試的一部分,不會(huì)被go build
編譯到最終的可執(zhí)行文件中。
在*_test.go
文件中有三種類(lèi)型的函數(shù),單元測(cè)試函數(shù)、基準(zhǔn)測(cè)試函數(shù)和示例函數(shù)。
類(lèi)型 | 格式 | 作用 |
---|---|---|
測(cè)試函數(shù) | 函數(shù)名前綴為T(mén)est | 測(cè)試程序的一些邏輯行為是否正確 |
基準(zhǔn)函數(shù) | 函數(shù)名前綴為Benchmark | 測(cè)試函數(shù)的性能 |
示例函數(shù) | 函數(shù)名前綴為Example | 為文檔提供示例文檔 |
go test
命令會(huì)遍歷所有的*_test.go
文件中符合上述命名規(guī)則的函數(shù),然后生成一個(gè)臨時(shí)的main包用于調(diào)用相應(yīng)的測(cè)試函數(shù),然后構(gòu)建并運(yùn)行、報(bào)告測(cè)試結(jié)果,最后清理測(cè)試中生成的臨時(shí)文件。
測(cè)試函數(shù)的格式
每個(gè)測(cè)試函數(shù)必須導(dǎo)入testing
包,測(cè)試函數(shù)的基本格式(簽名)如下:
func TestName(t *testing.T){ // ... }
測(cè)試函數(shù)的名字必須以Test
開(kāi)頭,可選的后綴名必須以大寫(xiě)字母開(kāi)頭,舉幾個(gè)例子:
func TestAdd(t *testing.T){ ... } func TestSum(t *testing.T){ ... } func TestLog(t *testing.T){ ... }
其中參數(shù)t
用于報(bào)告測(cè)試失敗和附加的日志信息。 testing.T
的擁有的方法如下:
func (c *T) Error(args ...interface{}) func (c *T) Errorf(format string, args ...interface{}) func (c *T) Fail() func (c *T) FailNow() func (c *T) Failed() bool func (c *T) Fatal(args ...interface{}) func (c *T) Fatalf(format string, args ...interface{}) func (c *T) Log(args ...interface{}) func (c *T) Logf(format string, args ...interface{}) func (c *T) Name() string func (t *T) Parallel() func (t *T) Run(name string, f func(t *T)) bool func (c *T) Skip(args ...interface{}) func (c *T) SkipNow() func (c *T) Skipf(format string, args ...interface{}) func (c *T) Skipped() bool
測(cè)試函數(shù)示例
就像細(xì)胞是構(gòu)成我們身體的基本單位,一個(gè)軟件程序也是由很多單元組件構(gòu)成的。單元組件可以是函數(shù)、結(jié)構(gòu)體、方法和最終用戶可能依賴(lài)的任意東西。總之我們需要確保這些組件是能夠正常運(yùn)行的。單元測(cè)試是一些利用各種方法測(cè)試單元組件的程序,它會(huì)將結(jié)果與預(yù)期輸出進(jìn)行比較。
接下來(lái),我們定義一個(gè)split
的包,包中定義了一個(gè)Split
函數(shù),具體實(shí)現(xiàn)如下:
// split/split.go package split import "strings" // split package with a single split function. // Split slices s into all substrings separated by sep and // returns a slice of the substrings between those separators. func Split(s, sep string) (result []string) { i := strings.Index(s, sep) for i > -1 { result = append(result, s[:i]) s = s[i+1:] i = strings.Index(s, sep) } result = append(result, s) return }
在當(dāng)前目錄下,我們創(chuàng)建一個(gè)split_test.go
的測(cè)試文件,并定義一個(gè)測(cè)試函數(shù)如下:
// split/split_test.go package split import ( "reflect" "testing" ) func TestSplit(t *testing.T) { // 測(cè)試函數(shù)名必須以Test開(kāi)頭,必須接收一個(gè)*testing.T類(lèi)型參數(shù) got := Split("a:b:c", ":") // 程序輸出的結(jié)果 want := []string{"a", "b", "c"} // 期望的結(jié)果 if !reflect.DeepEqual(want, got) { // 因?yàn)閟lice不能比較直接,借助反射包中的方法比較 t.Errorf("expected:%v, got:%v", want, got) // 測(cè)試失敗輸出錯(cuò)誤提示 } }
此時(shí)split
這個(gè)包中的文件如下:
split $ ls -l total 16 -rw-r--r-- 1 liwenzhou staff 408 4 29 15:50 split.go -rw-r--r-- 1 liwenzhou staff 466 4 29 16:04 split_test.go
在split
包路徑下,執(zhí)行go test
命令,可以看到輸出結(jié)果如下:
split $ go test PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s
一個(gè)測(cè)試用例有點(diǎn)單薄,我們?cè)倬帉?xiě)一個(gè)測(cè)試使用多個(gè)字符切割字符串的例子,在split_test.go
中添加如下測(cè)試函數(shù):
func TestMoreSplit(t *testing.T) { got := Split("abcd", "bc") want := []string{"a", "d"} if !reflect.DeepEqual(want, got) { t.Errorf("expected:%v, got:%v", want, got) } }
再次運(yùn)行go test
命令,輸出結(jié)果如下:
split $ go test --- FAIL: TestMultiSplit (0.00s) split_test.go:20: expected:[a d], got:[a cd] FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
這一次,我們的測(cè)試失敗了。我們可以為go test
命令添加-v
參數(shù),查看測(cè)試函數(shù)名稱(chēng)和運(yùn)行時(shí)間:
split $ go test -v === RUN TestSplit --- PASS: TestSplit (0.00s) === RUN TestMoreSplit --- FAIL: TestMoreSplit (0.00s) split_test.go:21: expected:[a d], got:[a cd] FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s
這一次我們能清楚的看到是TestMoreSplit
這個(gè)測(cè)試沒(méi)有成功。 還可以在go test
命令后添加-run
參數(shù),它對(duì)應(yīng)一個(gè)正則表達(dá)式,只有函數(shù)名匹配上的測(cè)試函數(shù)才會(huì)被go test
命令執(zhí)行。
split $ go test -v -run="More" === RUN TestMoreSplit --- FAIL: TestMoreSplit (0.00s) split_test.go:21: expected:[a d], got:[a cd] FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
現(xiàn)在我們回過(guò)頭來(lái)解決我們程序中的問(wèn)題。很顯然我們最初的split
函數(shù)并沒(méi)有考慮到sep為多個(gè)字符的情況,我們來(lái)修復(fù)下這個(gè)Bug:
package split import "strings" // split package with a single split function. // Split slices s into all substrings separated by sep and // returns a slice of the substrings between those separators. func Split(s, sep string) (result []string) { i := strings.Index(s, sep) for i > -1 { result = append(result, s[:i]) s = s[i+len(sep):] // 這里使用len(sep)獲取sep的長(zhǎng)度 i = strings.Index(s, sep) } result = append(result, s) return }
這一次我們?cè)賮?lái)測(cè)試一下,我們的程序。注意,當(dāng)我們修改了我們的代碼之后不要僅僅執(zhí)行那些失敗的測(cè)試函數(shù),我們應(yīng)該完整的運(yùn)行所有的測(cè)試,保證不會(huì)因?yàn)樾薷拇a而引入了新的問(wèn)題。
split $ go test -v === RUN TestSplit --- PASS: TestSplit (0.00s) === RUN TestMoreSplit --- PASS: TestMoreSplit (0.00s) PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
這一次我們的測(cè)試都通過(guò)了。
測(cè)試組
我們現(xiàn)在還想要測(cè)試一下split
函數(shù)對(duì)中文字符串的支持,這個(gè)時(shí)候我們可以再編寫(xiě)一個(gè)TestChineseSplit
測(cè)試函數(shù),但是我們也可以使用如下更友好的一種方式來(lái)添加更多的測(cè)試用例。
func TestSplit(t *testing.T) { // 定義一個(gè)測(cè)試用例類(lèi)型 type test struct { input string sep string want []string } // 定義一個(gè)存儲(chǔ)測(cè)試用例的切片 tests := []test{ {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}}, {input: "a:b:c", sep: ",", want: []string{"a:b:c"}}, {input: "abcd", sep: "bc", want: []string{"a", "d"}}, {input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}}, } // 遍歷切片,逐一執(zhí)行測(cè)試用例 for _, tc := range tests { got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf("expected:%v, got:%v", tc.want, got) } } }
我們通過(guò)上面的代碼把多個(gè)測(cè)試用例合到一起,再次執(zhí)行go test
命令。
split $ go test -v === RUN TestSplit --- FAIL: TestSplit (0.00s) split_test.go:42: expected:[河有 又有河], got:[ 河有 又有河] FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
我們的測(cè)試出現(xiàn)了問(wèn)題,仔細(xì)看打印的測(cè)試失敗提示信息:expected:[河有 又有河], got:[ 河有 又有河]
,你會(huì)發(fā)現(xiàn)[ 河有 又有河]
中有個(gè)不明顯的空串,這種情況下十分推薦使用%#v
的格式化方式。
我們修改下測(cè)試用例的格式化輸出錯(cuò)誤提示部分:
func TestSplit(t *testing.T) { ... for _, tc := range tests { got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf("expected:%#v, got:%#v", tc.want, got) } } }
此時(shí)運(yùn)行go test
命令后就能看到比較明顯的提示信息了:
split $ go test -v === RUN TestSplit --- FAIL: TestSplit (0.00s) split_test.go:42: expected:[]string{"河有", "又有河"}, got:[]string{"", "河有", "又有河"} FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
子測(cè)試
看起來(lái)都挺不錯(cuò)的,但是如果測(cè)試用例比較多的時(shí)候,我們是沒(méi)辦法一眼看出來(lái)具體是哪個(gè)測(cè)試用例失敗了。我們可能會(huì)想到下面的解決辦法:
func TestSplit(t *testing.T) { type test struct { // 定義test結(jié)構(gòu)體 input string sep string want []string } tests := map[string]test{ // 測(cè)試用例使用map存儲(chǔ) "simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}}, "wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}}, "more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}}, "leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}}, } for name, tc := range tests { got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf("name:%s expected:%#v, got:%#v", name, tc.want, got) // 將測(cè)試用例的name格式化輸出 } } }
上面的做法是能夠解決問(wèn)題的。同時(shí)Go1.7+中新增了子測(cè)試,我們可以按照如下方式使用t.Run
執(zhí)行子測(cè)試:
func TestSplit(t *testing.T) { type test struct { // 定義test結(jié)構(gòu)體 input string sep string want []string } tests := map[string]test{ // 測(cè)試用例使用map存儲(chǔ) "simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}}, "wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}}, "more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}}, "leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { // 使用t.Run()執(zhí)行子測(cè)試 got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf("expected:%#v, got:%#v", tc.want, got) } }) } }
此時(shí)我們?cè)賵?zhí)行go test
命令就能夠看到更清晰的輸出內(nèi)容了:
split $ go test -v === RUN TestSplit === RUN TestSplit/leading_sep === RUN TestSplit/simple === RUN TestSplit/wrong_sep === RUN TestSplit/more_sep --- FAIL: TestSplit (0.00s) --- FAIL: TestSplit/leading_sep (0.00s) split_test.go:83: expected:[]string{"河有", "又有河"}, got:[]string{"", "河有", "又有河"} --- PASS: TestSplit/simple (0.00s) --- PASS: TestSplit/wrong_sep (0.00s) --- PASS: TestSplit/more_sep (0.00s) FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
這個(gè)時(shí)候我們要把測(cè)試用例中的錯(cuò)誤修改回來(lái):
func TestSplit(t *testing.T) { ... tests := map[string]test{ // 測(cè)試用例使用map存儲(chǔ) "simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}}, "wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}}, "more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}}, "leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"", "河有", "又有河"}}, } ... }
我們都知道可以通過(guò)-run=RegExp
來(lái)指定運(yùn)行的測(cè)試用例,還可以通過(guò)/
來(lái)指定要運(yùn)行的子測(cè)試用例,例如:go test -v -run=Split/simple
只會(huì)運(yùn)行simple
對(duì)應(yīng)的子測(cè)試用例。
測(cè)試覆蓋率
測(cè)試覆蓋率是你的代碼被測(cè)試套件覆蓋的百分比。通常我們使用的都是語(yǔ)句的覆蓋率,也就是在測(cè)試中至少被運(yùn)行一次的代碼占總代碼的比例。
Go提供內(nèi)置功能來(lái)檢查你的代碼覆蓋率。我們可以使用go test -cover
來(lái)查看測(cè)試覆蓋率。例如:
split $ go test -cover PASS coverage: 100.0% of statements ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s
從上面的結(jié)果可以看到我們的測(cè)試用例覆蓋了100%的代碼。
Go還提供了一個(gè)額外的-coverprofile
參數(shù),用來(lái)將覆蓋率相關(guān)的記錄信息輸出到一個(gè)文件。例如:
split $ go test -cover -coverprofile=c.out PASS coverage: 100.0% of statements ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s
上面的命令會(huì)將覆蓋率相關(guān)的信息輸出到當(dāng)前文件夾下面的c.out
文件中,然后我們執(zhí)行go tool cover -html=c.out
,使用cover
工具來(lái)處理生成的記錄信息,該命令會(huì)打開(kāi)本地的瀏覽器窗口生成一個(gè)HTML報(bào)告。
上圖中每個(gè)用綠色標(biāo)記的語(yǔ)句塊表示被覆蓋了,而紅色的表示沒(méi)有被覆蓋。
2.基準(zhǔn)測(cè)試
基準(zhǔn)測(cè)試函數(shù)格式
基準(zhǔn)測(cè)試就是在一定的工作負(fù)載之下檢測(cè)程序性能的一種方法?;鶞?zhǔn)測(cè)試的基本格式如下:
func BenchmarkName(b *testing.B){ // ... }
基準(zhǔn)測(cè)試以Benchmark
為前綴,需要一個(gè)*testing.B
類(lèi)型的參數(shù)b,基準(zhǔn)測(cè)試必須要執(zhí)行b.N
次,這樣的測(cè)試才有對(duì)照性,b.N
的值是系統(tǒng)根據(jù)實(shí)際情況去調(diào)整的,從而保證測(cè)試的穩(wěn)定性。 testing.B
擁有的方法如下:
func (c *B) Error(args ...interface{}) func (c *B) Errorf(format string, args ...interface{}) func (c *B) Fail() func (c *B) FailNow() func (c *B) Failed() bool func (c *B) Fatal(args ...interface{}) func (c *B) Fatalf(format string, args ...interface{}) func (c *B) Log(args ...interface{}) func (c *B) Logf(format string, args ...interface{}) func (c *B) Name() string func (b *B) ReportAllocs() func (b *B) ResetTimer() func (b *B) Run(name string, f func(b *B)) bool func (b *B) RunParallel(body func(*PB)) func (b *B) SetBytes(n int64) func (b *B) SetParallelism(p int) func (c *B) Skip(args ...interface{}) func (c *B) SkipNow() func (c *B) Skipf(format string, args ...interface{}) func (c *B) Skipped() bool func (b *B) StartTimer() func (b *B) StopTimer()
基準(zhǔn)測(cè)試示例
我們?yōu)閟plit包中的Split
函數(shù)編寫(xiě)基準(zhǔn)測(cè)試如下:
func BenchmarkSplit(b *testing.B) { for i := 0; i < b.N; i++ { Split("沙河有沙又有河", "沙") } }
基準(zhǔn)測(cè)試并不會(huì)默認(rèn)執(zhí)行,需要增加-bench
參數(shù),所以我們通過(guò)執(zhí)行go test -bench=Split
命令執(zhí)行基準(zhǔn)測(cè)試,輸出結(jié)果如下:
split $ go test -bench=Split goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/split BenchmarkSplit-8 10000000 203 ns/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 2.255s
其中BenchmarkSplit-8
表示對(duì)Split函數(shù)進(jìn)行基準(zhǔn)測(cè)試,數(shù)字8
表示GOMAXPROCS
的值,這個(gè)對(duì)于并發(fā)基準(zhǔn)測(cè)試很重要。10000000
和203ns/op
表示每次調(diào)用Split
函數(shù)耗時(shí)203ns
,這個(gè)結(jié)果是10000000
次調(diào)用的平均值。
我們還可以為基準(zhǔn)測(cè)試添加-benchmem
參數(shù),來(lái)獲得內(nèi)存分配的統(tǒng)計(jì)數(shù)據(jù)。
split $ go test -bench=Split -benchmem goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/split BenchmarkSplit-8 10000000 215 ns/op 112 B/op 3 allocs/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 2.394s
其中,112 B/op
表示每次操作內(nèi)存分配了112字節(jié),3 allocs/op
則表示每次操作進(jìn)行了3次內(nèi)存分配。 我們將我們的Split
函數(shù)優(yōu)化如下:
func Split(s, sep string) (result []string) { result = make([]string, 0, strings.Count(s, sep)+1) i := strings.Index(s, sep) for i > -1 { result = append(result, s[:i]) s = s[i+len(sep):] // 這里使用len(sep)獲取sep的長(zhǎng)度 i = strings.Index(s, sep) } result = append(result, s) return }
這一次我們提前使用make函數(shù)將result初始化為一個(gè)容量足夠大的切片,而不再像之前一樣通過(guò)調(diào)用append函數(shù)來(lái)追加。我們來(lái)看一下這個(gè)改進(jìn)會(huì)帶來(lái)多大的性能提升:
split $ go test -bench=Split -benchmem goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/split BenchmarkSplit-8 10000000 127 ns/op 48 B/op 1 allocs/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 1.423s
這個(gè)使用make函數(shù)提前分配內(nèi)存的改動(dòng),減少了2/3的內(nèi)存分配次數(shù),并且減少了一半的內(nèi)存分配。
性能比較函數(shù)
上面的基準(zhǔn)測(cè)試只能得到給定操作的絕對(duì)耗時(shí),但是在很多性能問(wèn)題是發(fā)生在兩個(gè)不同操作之間的相對(duì)耗時(shí),比如同一個(gè)函數(shù)處理1000個(gè)元素的耗時(shí)與處理1萬(wàn)甚至100萬(wàn)個(gè)元素的耗時(shí)的差別是多少?再或者對(duì)于同一個(gè)任務(wù)究竟使用哪種算法性能最佳?我們通常需要對(duì)兩個(gè)不同算法的實(shí)現(xiàn)使用相同的輸入來(lái)進(jìn)行基準(zhǔn)比較測(cè)試。
性能比較函數(shù)通常是一個(gè)帶有參數(shù)的函數(shù),被多個(gè)不同的Benchmark函數(shù)傳入不同的值來(lái)調(diào)用。舉個(gè)例子如下:
func benchmark(b *testing.B, size int){/* ... */} func Benchmark10(b *testing.B){ benchmark(b, 10) } func Benchmark100(b *testing.B){ benchmark(b, 100) } func Benchmark1000(b *testing.B){ benchmark(b, 1000) }
例如我們編寫(xiě)了一個(gè)計(jì)算斐波那契數(shù)列的函數(shù)如下:
// fib.go // Fib 是一個(gè)計(jì)算第n個(gè)斐波那契數(shù)的函數(shù) func Fib(n int) int { if n < 2 { return n } return Fib(n-1) + Fib(n-2) }
我們編寫(xiě)的性能比較函數(shù)如下:
// fib_test.go func benchmarkFib(b *testing.B, n int) { for i := 0; i < b.N; i++ { Fib(n) } } func BenchmarkFib1(b *testing.B) { benchmarkFib(b, 1) } func BenchmarkFib2(b *testing.B) { benchmarkFib(b, 2) } func BenchmarkFib3(b *testing.B) { benchmarkFib(b, 3) } func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) } func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) } func BenchmarkFib40(b *testing.B) { benchmarkFib(b, 40) }
運(yùn)行基準(zhǔn)測(cè)試:
split $ go test -bench=. goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/fib BenchmarkFib1-8 1000000000 2.03 ns/op BenchmarkFib2-8 300000000 5.39 ns/op BenchmarkFib3-8 200000000 9.71 ns/op BenchmarkFib10-8 5000000 325 ns/op BenchmarkFib20-8 30000 42460 ns/op BenchmarkFib40-8 2 638524980 ns/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/fib 12.944s
這里需要注意的是,默認(rèn)情況下,每個(gè)基準(zhǔn)測(cè)試至少運(yùn)行1秒。如果在Benchmark函數(shù)返回時(shí)沒(méi)有到1秒,則b.N的值會(huì)按1,2,5,10,20,50,…增加,并且函數(shù)再次運(yùn)行。
最終的BenchmarkFib40只運(yùn)行了兩次,每次運(yùn)行的平均值只有不到一秒。像這種情況下我們應(yīng)該可以使用-benchtime
標(biāo)志增加最小基準(zhǔn)時(shí)間,以產(chǎn)生更準(zhǔn)確的結(jié)果。例如:
split $ go test -bench=Fib40 -benchtime=20s goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/fib BenchmarkFib40-8 50 663205114 ns/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/fib 33.849s
這一次BenchmarkFib40
函數(shù)運(yùn)行了50次,結(jié)果就會(huì)更準(zhǔn)確一些了。
使用性能比較函數(shù)做測(cè)試的時(shí)候一個(gè)容易犯的錯(cuò)誤就是把b.N
作為輸入的大小,例如以下兩個(gè)例子都是錯(cuò)誤的示范:
// 錯(cuò)誤示范1 func BenchmarkFibWrong(b *testing.B) { for n := 0; n < b.N; n++ { Fib(n) } } // 錯(cuò)誤示范2 func BenchmarkFibWrong2(b *testing.B) { Fib(b.N) }
重置時(shí)間
b.ResetTimer
之前的處理不會(huì)放到執(zhí)行時(shí)間里,也不會(huì)輸出到報(bào)告中,所以可以在之前做一些不計(jì)劃作為測(cè)試報(bào)告的操作。例如:
func BenchmarkSplit(b *testing.B) { time.Sleep(5 * time.Second) // 假設(shè)需要做一些耗時(shí)的無(wú)關(guān)操作 b.ResetTimer() // 重置計(jì)時(shí)器 for i := 0; i < b.N; i++ { Split("沙河有沙又有河", "沙") } }
并行測(cè)試
func (b *B) RunParallel(body func(*PB))
會(huì)以并行的方式執(zhí)行給定的基準(zhǔn)測(cè)試。
RunParallel
會(huì)創(chuàng)建出多個(gè)goroutine
,并將b.N
分配給這些goroutine
執(zhí)行, 其中goroutine
數(shù)量的默認(rèn)值為GOMAXPROCS
。用戶如果想要增加非CPU受限(non-CPU-bound)基準(zhǔn)測(cè)試的并行性, 那么可以在RunParallel
之前調(diào)用SetParallelism
。RunParallel
通常會(huì)與-cpu
標(biāo)志一同使用。
func BenchmarkSplitParallel(b *testing.B) { // b.SetParallelism(1) // 設(shè)置使用的CPU數(shù) b.RunParallel(func(pb *testing.PB) { for pb.Next() { Split("沙河有沙又有河", "沙") } }) }
執(zhí)行一下基準(zhǔn)測(cè)試:
split $ go test -bench=. goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/split BenchmarkSplit-8 10000000 131 ns/op BenchmarkSplitParallel-8 50000000 36.1 ns/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 3.308s
還可以通過(guò)在測(cè)試命令后添加-cpu
參數(shù)如go test -bench=. -cpu 1
來(lái)指定使用的CPU數(shù)量。
3.Setup與TearDown
測(cè)試程序有時(shí)需要在測(cè)試之前進(jìn)行額外的設(shè)置(setup)或在測(cè)試之后進(jìn)行拆卸(teardown)。
TestMain
通過(guò)在*_test.go
文件中定義TestMain
函數(shù)來(lái)可以在測(cè)試之前進(jìn)行額外的設(shè)置(setup)或在測(cè)試之后進(jìn)行拆卸(teardown)操作。
如果測(cè)試文件包含函數(shù):func TestMain(m *testing.M)
那么生成的測(cè)試會(huì)先調(diào)用 TestMain(m),然后再運(yùn)行具體測(cè)試。TestMain
運(yùn)行在主goroutine
中, 可以在調(diào)用 m.Run
前后做任何設(shè)置(setup)和拆卸(teardown)。退出測(cè)試的時(shí)候應(yīng)該使用m.Run
的返回值作為參數(shù)調(diào)用os.Exit
。
一個(gè)使用TestMain
來(lái)設(shè)置Setup和TearDown的示例如下:
func TestMain(m *testing.M) { fmt.Println("write setup code here...") // 測(cè)試之前的做一些設(shè)置 // 如果 TestMain 使用了 flags,這里應(yīng)該加上flag.Parse() retCode := m.Run() // 執(zhí)行測(cè)試 fmt.Println("write teardown code here...") // 測(cè)試之后做一些拆卸工作 os.Exit(retCode) // 退出測(cè)試 }
需要注意的是:在調(diào)用TestMain
時(shí), flag.Parse
并沒(méi)有被調(diào)用。所以如果TestMain
依賴(lài)于command-line標(biāo)志 (包括 testing 包的標(biāo)記), 則應(yīng)該顯示的調(diào)用flag.Parse
。
子測(cè)試的Setup與Teardown
有時(shí)候我們可能需要為每個(gè)測(cè)試集設(shè)置Setup與Teardown,也有可能需要為每個(gè)子測(cè)試設(shè)置Setup與Teardown。下面我們定義兩個(gè)函數(shù)工具函數(shù)如下:
// 測(cè)試集的Setup與Teardown func setupTestCase(t *testing.T) func(t *testing.T) { t.Log("如有需要在此執(zhí)行:測(cè)試之前的setup") return func(t *testing.T) { t.Log("如有需要在此執(zhí)行:測(cè)試之后的teardown") } } // 子測(cè)試的Setup與Teardown func setupSubTest(t *testing.T) func(t *testing.T) { t.Log("如有需要在此執(zhí)行:子測(cè)試之前的setup") return func(t *testing.T) { t.Log("如有需要在此執(zhí)行:子測(cè)試之后的teardown") } }
使用方式如下:
func TestSplit(t *testing.T) { type test struct { // 定義test結(jié)構(gòu)體 input string sep string want []string } tests := map[string]test{ // 測(cè)試用例使用map存儲(chǔ) "simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}}, "wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}}, "more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}}, "leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"", "河有", "又有河"}}, } teardownTestCase := setupTestCase(t) // 測(cè)試之前執(zhí)行setup操作 defer teardownTestCase(t) // 測(cè)試之后執(zhí)行testdoen操作 for name, tc := range tests { t.Run(name, func(t *testing.T) { // 使用t.Run()執(zhí)行子測(cè)試 teardownSubTest := setupSubTest(t) // 子測(cè)試之前執(zhí)行setup操作 defer teardownSubTest(t) // 測(cè)試之后執(zhí)行testdoen操作 got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf("expected:%#v, got:%#v", tc.want, got) } }) } }
測(cè)試結(jié)果如下:
split $ go test -v === RUN TestSplit === RUN TestSplit/simple === RUN TestSplit/wrong_sep === RUN TestSplit/more_sep === RUN TestSplit/leading_sep --- PASS: TestSplit (0.00s) split_test.go:71: 如有需要在此執(zhí)行:測(cè)試之前的setup --- PASS: TestSplit/simple (0.00s) split_test.go:79: 如有需要在此執(zhí)行:子測(cè)試之前的setup split_test.go:81: 如有需要在此執(zhí)行:子測(cè)試之后的teardown --- PASS: TestSplit/wrong_sep (0.00s) split_test.go:79: 如有需要在此執(zhí)行:子測(cè)試之前的setup split_test.go:81: 如有需要在此執(zhí)行:子測(cè)試之后的teardown --- PASS: TestSplit/more_sep (0.00s) split_test.go:79: 如有需要在此執(zhí)行:子測(cè)試之前的setup split_test.go:81: 如有需要在此執(zhí)行:子測(cè)試之后的teardown --- PASS: TestSplit/leading_sep (0.00s) split_test.go:79: 如有需要在此執(zhí)行:子測(cè)試之前的setup split_test.go:81: 如有需要在此執(zhí)行:子測(cè)試之后的teardown split_test.go:73: 如有需要在此執(zhí)行:測(cè)試之后的teardown === RUN ExampleSplit --- PASS: ExampleSplit (0.00s) PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
4.示例函數(shù)
示例函數(shù)的格式
被go test
特殊對(duì)待的第三種函數(shù)就是示例函數(shù),它們的函數(shù)名以Example
為前綴。它們既沒(méi)有參數(shù)也沒(méi)有返回值。標(biāo)準(zhǔn)格式如下:
func ExampleName() { // ... }
示例函數(shù)示例
下面的代碼是我們?yōu)?code>Split函數(shù)編寫(xiě)的一個(gè)示例函數(shù):
func ExampleSplit() { fmt.Println(split.Split("a:b:c", ":")) fmt.Println(split.Split("沙河有沙又有河", "沙")) // Output: // [a b c] // [ 河有 又有河] }
為你的代碼編寫(xiě)示例代碼有如下三個(gè)用處:
示例函數(shù)能夠作為文檔直接使用,例如基于web的godoc中能把示例函數(shù)與對(duì)應(yīng)的函數(shù)或包相關(guān)聯(lián)。
示例函數(shù)只要包含了
// Output:
也是可以通過(guò)go test
運(yùn)行的可執(zhí)行測(cè)試。
split $ go test -run Example PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
示例函數(shù)提供了可以直接運(yùn)行的示例代碼,可以直接在golang.org
的godoc
文檔服務(wù)器上使用Go Playground
運(yùn)行示例代碼。下圖為strings.ToUpper
函數(shù)在Playground的示例函數(shù)效果。
package main import ( "fmt" "strings") func main() { fmt.Println(strings.ToUpper("Go Upper")) }
到此這篇關(guān)于Go語(yǔ)言單元測(cè)試的實(shí)現(xiàn)及用例的文章就介紹到這了,更多相關(guān)Go語(yǔ)言單元測(cè)試內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Go語(yǔ)言單元測(cè)試超詳細(xì)解析
- Go語(yǔ)言單元測(cè)試基礎(chǔ)從入門(mén)到放棄
- 一文帶你了解Go語(yǔ)言中的單元測(cè)試
- 一文詳解Go語(yǔ)言單元測(cè)試的原理與使用
- Go語(yǔ)言工程實(shí)踐單元測(cè)試基準(zhǔn)測(cè)試示例詳解
- go語(yǔ)言單元測(cè)試基準(zhǔn)測(cè)試及表驅(qū)動(dòng)測(cè)試示例詳解
- 詳解在Go語(yǔ)言單元測(cè)試中如何解決文件依賴(lài)問(wèn)題
- Go語(yǔ)言:打造優(yōu)雅數(shù)據(jù)庫(kù)單元測(cè)試的實(shí)戰(zhàn)指南
相關(guān)文章
Go語(yǔ)言實(shí)現(xiàn)二進(jìn)制與十進(jìn)制互轉(zhuǎn)的示例代碼
這篇文章主要和大家詳細(xì)介紹了Go語(yǔ)言中實(shí)現(xiàn)二進(jìn)制與十進(jìn)制互相轉(zhuǎn)換的示例代碼,文中的代碼簡(jiǎn)潔易懂,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-05-05Go實(shí)現(xiàn)map并發(fā)安全的3種方式總結(jié)
Go的原生map不是并發(fā)安全的,在多協(xié)程讀寫(xiě)同一個(gè)map的時(shí)候,安全性無(wú)法得到保障,這篇文章主要給大家總結(jié)介紹了關(guān)于Go實(shí)現(xiàn)map并發(fā)安全的3種方式,需要的朋友可以參考下2023-10-10Golang中文字符串截取函數(shù)實(shí)現(xiàn)原理
在golang中可以通過(guò)切片截取一個(gè)數(shù)組或字符串,但是當(dāng)截取的字符串是中文時(shí),可能會(huì)出現(xiàn)問(wèn)題,下面我們來(lái)自定義個(gè)函數(shù)解決Golang中文字符串截取問(wèn)題2018-03-03go-zero使用goctl生成mongodb的操作使用方法
mongodb是一種高性能、開(kāi)源、文檔型的nosql數(shù)據(jù)庫(kù),被廣泛應(yīng)用于web應(yīng)用、大數(shù)據(jù)以及云計(jì)算領(lǐng)域,goctl model 為 goctl 提供的數(shù)據(jù)庫(kù)模型代碼生成指令,目前支持 MySQL、PostgreSQL、Mongo 的代碼生成,本文給大家介紹了go-zero使用goctl生成mongodb的操作使用方法2024-06-06GScript?編寫(xiě)標(biāo)準(zhǔn)庫(kù)示例詳解
這篇文章主要為大家介紹了GScript?編寫(xiě)標(biāo)準(zhǔn)庫(kù)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10Go與Rust高性能解析JSON實(shí)現(xiàn)方法示例
這篇文章主要為大家介紹了Go與Rust高性能的解析JSON實(shí)現(xiàn)方法示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12