亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Go語(yǔ)言單元測(cè)試的實(shí)現(xiàn)及用例

 更新時(shí)間:2024年01月28日 16:16:33   作者:leellun  
在日常開(kāi)發(fā)中,我們通常需要針對(duì)現(xiàn)有的功能進(jìn)行單元測(cè)試,以驗(yàn)證開(kāi)發(fā)的正確性,本文主要介紹了Go語(yǔ)言單元測(cè)試的實(shí)現(xiàn)及用例,具有一定的參考價(jià)值,感興趣的可以了解一下

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)告。

Go test cover

上圖中每個(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è)試很重要。10000000203ns/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.orggodoc文檔服務(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)文章希望大家以后多多支持腳本之家! 

相關(guān)文章

  • Go語(yǔ)言應(yīng)用閉包之返回函數(shù)

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

    這篇文章主要介紹了Go語(yǔ)言應(yīng)用閉包之返回函數(shù),對(duì)于非常底層的純 Go 語(yǔ)言代碼或者包而言,在各個(gè)操作系統(tǒng)平臺(tái)上的可移植性是非常強(qiáng)的,只需要將源碼拷貝到相應(yīng)平臺(tái)上進(jìn)行編譯即可,或者可以使用交叉編譯來(lái)構(gòu)建目標(biāo)平臺(tái)的應(yīng)用程序,需要的朋友可以參考下
    2023-07-07
  • Go語(yǔ)言實(shí)現(xiàn)二進(jìn)制與十進(jìn)制互轉(zhuǎ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-05
  • Go1.21新增slices包的用法詳解

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

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

    Go實(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-10
  • Golang中文字符串截取函數(shù)實(shí)現(xiàn)原理

    Golang中文字符串截取函數(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-03
  • go-zero使用goctl生成mongodb的操作使用方法

    go-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-06
  • 詳解Golang語(yǔ)言中的interface

    詳解Golang語(yǔ)言中的interface

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

    GScript?編寫(xiě)標(biāo)準(zhǔn)庫(kù)示例詳解

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

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

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

    GO語(yǔ)言Defer用法實(shí)例分析

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

最新評(píng)論