欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Go語言單元測試的實現(xiàn)及用例

 更新時間:2024年01月28日 16:16:33   作者:leellun  
在日常開發(fā)中,我們通常需要針對現(xiàn)有的功能進行單元測試,以驗證開發(fā)的正確性,本文主要介紹了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報告。

Go test cover

上圖中每個用綠色標記的語句塊表示被覆蓋了,而紅色的表示沒有被覆蓋。

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ā)基準測試很重要。10000000203ns/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.orggodoc文檔服務(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語言應用閉包之返回函數(shù)

    Go語言應用閉包之返回函數(shù)

    這篇文章主要介紹了Go語言應用閉包之返回函數(shù),對于非常底層的純 Go 語言代碼或者包而言,在各個操作系統(tǒng)平臺上的可移植性是非常強的,只需要將源碼拷貝到相應平臺上進行編譯即可,或者可以使用交叉編譯來構(gòu)建目標平臺的應用程序,需要的朋友可以參考下
    2023-07-07
  • Go語言實現(xiàn)二進制與十進制互轉(zhuǎn)的示例代碼

    Go語言實現(xiàn)二進制與十進制互轉(zhuǎn)的示例代碼

    這篇文章主要和大家詳細介紹了Go語言中實現(xiàn)二進制與十進制互相轉(zhuǎn)換的示例代碼,文中的代碼簡潔易懂,感興趣的小伙伴可以跟隨小編一起學習一下
    2023-05-05
  • Go1.21新增slices包的用法詳解

    Go1.21新增slices包的用法詳解

    Go?1.21新增的?slices?包提供了很多和切片相關(guān)的函數(shù),可以用于任何類型的切片,這篇文章主要來和大家介紹一下slices包中相關(guān)函數(shù)的用法,需要的可以參考一下
    2023-08-08
  • Go實現(xiàn)map并發(fā)安全的3種方式總結(jié)

    Go實現(xiàn)map并發(fā)安全的3種方式總結(jié)

    Go的原生map不是并發(fā)安全的,在多協(xié)程讀寫同一個map的時候,安全性無法得到保障,這篇文章主要給大家總結(jié)介紹了關(guān)于Go實現(xiàn)map并發(fā)安全的3種方式,需要的朋友可以參考下
    2023-10-10
  • Golang中文字符串截取函數(shù)實現(xiàn)原理

    Golang中文字符串截取函數(shù)實現(xiàn)原理

    在golang中可以通過切片截取一個數(shù)組或字符串,但是當截取的字符串是中文時,可能會出現(xiàn)問題,下面我們來自定義個函數(shù)解決Golang中文字符串截取問題
    2018-03-03
  • go-zero使用goctl生成mongodb的操作使用方法

    go-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
  • 詳解Golang語言中的interface

    詳解Golang語言中的interface

    這篇文章主要介紹了Golang語言中的interface的相關(guān)資料,幫助大家更好的理解和使用golang,感興趣的朋友可以了解下
    2021-01-01
  • GScript?編寫標準庫示例詳解

    GScript?編寫標準庫示例詳解

    這篇文章主要為大家介紹了GScript?編寫標準庫示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-10-10
  • Go與Rust高性能解析JSON實現(xiàn)方法示例

    Go與Rust高性能解析JSON實現(xiàn)方法示例

    這篇文章主要為大家介紹了Go與Rust高性能的解析JSON實現(xiàn)方法示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-12-12
  • GO語言Defer用法實例分析

    GO語言Defer用法實例分析

    這篇文章主要介紹了GO語言Defer用法,實例分析了Defer的使用技巧,具有一定參考借鑒價值,需要的朋友可以參考下
    2015-02-02

最新評論