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