Go語言基礎之單元測試



更新、更全的《Go從入門到放棄》的更新網站,更有python、go、人工智能教學等着你: https://www.cnblogs.com/nickchen121/p/11517502.html

不寫測試的開發不是好程序員。我個人非常崇尚TDD(Test Driven Development)的,然而可惜的是國內的程序員都不太關注測試這一部分。 這篇文章主要介紹下在Go語言中如何做單元測試和基准測試。

一、go test工具

Go語言中的測試依賴go test命令。編寫測試代碼和編寫普通的Go代碼過程是類似的,並不需要學習新的語法、規則或工具。

go test命令是一個按照一定約定和組織的測試代碼的驅動程序。在包目錄內,所有以_test.go為后綴名的源代碼文件都是go test測試的一部分,不會被go build編譯到最終的可執行文件中。

*_test.go文件中有三種類型的函數,單元測試函數、基准測試函數和示例函數。

類型 格式 作用
測試函數 函數名前綴為Test 測試程序的一些邏輯行為是否正確
基准函數 函數名前綴為Benchmark 測試函數的性能
示例函數 函數名前綴為Example 為文檔提供示例文檔

go test命令會遍歷所有的*_test.go文件中符合上述命名規則的函數,然后生成一個臨時的main包用於調用相應的測試函數,然后構建並運行、報告測試結果,最后清理測試中生成的臨時文件。

二、測試函數

三、測試函數的格式

每個測試函數必須導入testing包,測試函數的基本格式(簽名)如下:

func TestName(t *testing.T){
    // ...
}

測試函數的名字必須以Test開頭,可選的后綴名必須以大寫字母開頭,舉幾個例子:

func TestAdd(t *testing.T){ ... }
func TestSum(t *testing.T){ ... }
func TestLog(t *testing.T){ ... }

其中參數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

四、測試函數示例

就像細胞是構成我們身體的基本單位,一個軟件程序也是由很多單元組件構成的。單元組件可以是函數、結構體、方法和最終用戶可能依賴的任意東西。總之我們需要確保這些組件是能夠正常運行的。單元測試是一些利用各種方法測試單元組件的程序,它會將結果與預期輸出進行比較。

接下來,我們定義一個split的包,包中定義了一個Split函數,具體實現如下:

// 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
}

在當前目錄下,我們創建一個split_test.go的測試文件,並定義一個測試函數如下:

// split/split_test.go

package split

import (
	"reflect"
	"testing"
)

func TestSplit(t *testing.T) { // 測試函數名必須以Test開頭,必須接收一個*testing.T類型參數
	got := Split("a:b:c", ":")         // 程序輸出的結果
	want := []string{"a", "b", "c"}    // 期望的結果
	if !reflect.DeepEqual(want, got) { // 因為slice不能比較直接,借助反射包中的方法比較
		t.Errorf("excepted:%v, got:%v", want, got) // 測試失敗輸出錯誤提示
	}
}

此時split這個包中的文件如下:

split $ ls -l
total 16
-rw-r--r--  1 nickchen121  staff  408  4 29 15:50 split.go
-rw-r--r--  1 nickchen121  staff  466  4 29 16:04 split_test.go

split包路徑下,執行go test命令,可以看到輸出結果如下:

split $ go test
PASS
ok      github.com/Q1mi/studygo/code_demo/test_demo/split       0.005s

一個測試用例有點單薄,我們再編寫一個測試使用多個字符切割字符串的例子,在split_test.go中添加如下測試函數:

func TestMoreSplit(t *testing.T) {
	got := Split("abcd", "bc")
	want := []string{"a", "d"}
	if !reflect.DeepEqual(want, got) {
		t.Errorf("excepted:%v, got:%v", want, got)
	}
}

再次運行go test命令,輸出結果如下:

split $ go test
--- FAIL: TestMultiSplit (0.00s)
    split_test.go:20: excepted:[a d], got:[a cd]
FAIL
exit status 1
FAIL    github.com/Q1mi/studygo/code_demo/test_demo/split       0.006s

這一次,我們的測試失敗了。我們可以為go test命令添加-v參數,查看測試函數名稱和運行時間:

split $ go test -v
=== RUN   TestSplit
--- PASS: TestSplit (0.00s)
=== RUN   TestMoreSplit
--- FAIL: TestMoreSplit (0.00s)
    split_test.go:21: excepted:[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參數,它對應一個正則表達式,只有函數名匹配上的測試函數才會被go test命令執行。

split $ go test -v -run="More"
=== RUN   TestMoreSplit
--- FAIL: TestMoreSplit (0.00s)
    split_test.go:21: excepted:[a d], got:[a cd]
FAIL
exit status 1
FAIL    github.com/Q1mi/studygo/code_demo/test_demo/split       0.006s

現在我們回過頭來解決我們程序中的問題。很顯然我們最初的split函數並沒有考慮到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
}

這一次我們再來測試一下,我們的程序。注意,當我們修改了我們的代碼之后不要僅僅執行那些失敗的測試函數,我們應該完整的運行所有的測試,保證不會因為修改代碼而引入了新的問題。

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

這一次我們的測試都通過了。

五、測試組

我們現在還想要測試一下split函數對中文字符串的支持,這個時候我們可以再編寫一個TestChineseSplit測試函數,但是我們也可以使用如下更友好的一種方式來添加更多的測試用例。

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{"河有", "又有河"}},
	}
	// 遍歷切片,逐一執行測試用例
	for _, tc := range tests {
		got := Split(tc.input, tc.sep)
		if !reflect.DeepEqual(got, tc.want) {
			t.Errorf("excepted:%v, got:%v", tc.want, got)
		}
	}
}

我們通過上面的代碼把多個測試用例合到一起,再次執行go test命令。

split $ go test -v
=== RUN   TestSplit
--- FAIL: TestSplit (0.00s)
    split_test.go:42: excepted:[河有 又有河], got:[ 河有 又有河]
FAIL
exit status 1
FAIL    github.com/Q1mi/studygo/code_demo/test_demo/split       0.006s

我們的測試出現了問題,仔細看打印的測試失敗提示信息:excepted:[河有 又有河], got:[ 河有 又有河],你會發現[ 河有 又有河]中有個不明顯的空串,這種情況下十分推薦使用%#v的格式化方式。

我們修改下測試用例的格式化輸出錯誤提示部分:

func TestSplit(t *testing.T) {
   ...
   
	for _, tc := range tests {
		got := Split(tc.input, tc.sep)
		if !reflect.DeepEqual(got, tc.want) {
			t.Errorf("excepted:%#v, got:%#v", tc.want, got)
		}
	}
}

此時運行go test命令后就能看到比較明顯的提示信息了:

split $ go test -v
=== RUN   TestSplit
--- FAIL: TestSplit (0.00s)
    split_test.go:42: excepted:[]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結構體
		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 excepted:%#v, got:%#v", name, tc.want, got) // 將測試用例的name格式化輸出
		}
	}
}

上面的做法是能夠解決問題的。同時Go1.7+中新增了子測試,我們可以按照如下方式使用t.Run執行子測試:

func TestSplit(t *testing.T) {
	type test struct { // 定義test結構體
		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()執行子測試
			got := Split(tc.input, tc.sep)
			if !reflect.DeepEqual(got, tc.want) {
				t.Errorf("excepted:%#v, got:%#v", tc.want, got)
			}
		})
	}
}

此時我們再執行go test命令就能夠看到更清晰的輸出內容了:

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: excepted:[]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提供內置功能來檢查你的代碼覆蓋率。我們可以使用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

從上面的結果可以看到我們的測試用例覆蓋了100%的代碼。

Go還提供了一個額外的-coverprofile參數,用來將覆蓋率相關的記錄信息輸出到一個文件。例如:

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

上面的命令會將覆蓋率相關的信息輸出到當前文件夾下面的c.out文件中,然后我們執行go tool cover -html=c.out,使用cover工具來處理生成的記錄信息,該命令會打開本地的瀏覽器窗口生成一個HTML報告。 cover.png 上圖中每個用綠色標記的語句塊表示被覆蓋了,而紅色的表示沒有被覆蓋。

八、基准測試

九、基准測試函數格式

基准測試就是在一定的工作負載之下檢測程序性能的一種方法。基准測試的基本格式如下:

func BenchmarkName(b *testing.B){
    // ...
}

基准測試以Benchmark為前綴,需要一個*testing.B類型的參數b,基准測試必須要執行b.N次,這樣的測試才有對照性,b.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()

十、基准測試示例

我們為split包中的Split函數編寫基准測試如下:

func BenchmarkSplit(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Split("沙河有沙又有河", "沙")
	}
}

基准測試並不會默認執行,需要增加-bench參數,所以我們通過執行go test -bench=Split命令執行基准測試,輸出結果如下:

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函數進行基准測試,數字8表示GOMAXPROCS的值,這個對於並發基准測試很重要。10000000203ns/op表示每次調用Split函數耗時203ns,這個結果是10000000次調用的平均值。

我們還可以為基准測試添加-benchmem參數,來獲得內存分配的統計數據。

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表示每次操作內存分配了112字節,3 allocs/op則表示每次操作進行了3次內存分配。 我們將我們的Split函數優化如下:

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函數將result初始化為一個容量足夠大的切片,而不再像之前一樣通過調用append函數來追加。我們來看一下這個改進會帶來多大的性能提升:

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函數提前分配內存的改動,減少了2/3的內存分配次數,並且減少了一半的內存分配。

十一、性能比較函數

上面的基准測試只能得到給定操作的絕對耗時,但是在很多性能問題是發生在兩個不同操作之間的相對耗時,比如同一個函數處理1000個元素的耗時與處理1萬甚至100萬個元素的耗時的差別是多少?再或者對於同一個任務究竟使用哪種算法性能最佳?我們通常需要對兩個不同算法的實現使用相同的輸入來進行基准比較測試。

性能比較函數通常是一個帶有參數的函數,被多個不同的Benchmark函數傳入不同的值來調用。舉個例子如下:

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) }

例如我們編寫了一個計算斐波那契數列的函數如下:

// fib.go

// Fib 是一個計算第n個斐波那契數的函數
func Fib(n int) int {
	if n < 2 {
		return n
	}
	return Fib(n-1) + Fib(n-2)
}

我們編寫的性能比較函數如下:

// 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函數返回時沒有到1秒,則b.N的值會按1,2,5,10,20,50,…增加,並且函數再次運行。

最終的BenchmarkFib40只運行了兩次,每次運行的平均值只有不到一秒。像這種情況下我們應該可以使用-benchtime標志增加最小基准時間,以產生更准確的結果。例如:

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函數運行了50次,結果就會更准確一些了。

使用性能比較函數做測試的時候一個容易犯的錯誤就是把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之前的處理不會放到執行時間里,也不會輸出到報告中,所以可以在之前做一些不計划作為測試報告的操作。例如:

func BenchmarkSplit(b *testing.B) {
	time.Sleep(5 * time.Second) // 假設需要做一些耗時的無關操作
	b.ResetTimer()              // 重置計時器
	for i := 0; i < b.N; i++ {
		Split("沙河有沙又有河", "沙")
	}
}

十三、並行測試

func (b *B) RunParallel(body func(*PB))會以並行的方式執行給定的基准測試。

RunParallel會創建出多個goroutine,並將b.N分配給這些goroutine執行, 其中goroutine數量的默認值為GOMAXPROCS。用戶如果想要增加非CPU受限(non-CPU-bound)基准測試的並行性, 那么可以在RunParallel之前調用SetParallelismRunParallel通常會與-cpu標志一同使用。

func BenchmarkSplitParallel(b *testing.B) {
	// b.SetParallelism(1) // 設置使用的CPU數
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			Split("沙河有沙又有河", "沙")
		}
	})
}

執行一下基准測試:

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參數如go test -bench=. -cpu 1來指定使用的CPU數量。

十四、Setup與TearDown

測試程序有時需要在測試之前進行額外的設置(setup)或在測試之后進行拆卸(teardown)。

十五、TestMain

通過在*_test.go文件中定義TestMain函數來可以在測試之前進行額外的設置(setup)或在測試之后進行拆卸(teardown)操作。

如果測試文件包含函數:func TestMain(m *testing.M)那么生成的測試會先調用 TestMain(m),然后再運行具體測試。TestMain運行在主goroutine中, 可以在調用 m.Run前后做任何設置(setup)和拆卸(teardown)。退出測試的時候應該使用m.Run的返回值作為參數調用os.Exit

一個使用TestMain來設置Setup和TearDown的示例如下:

func TestMain(m *testing.M) {
	fmt.Println("write setup code here...") // 測試之前的做一些設置
	// 如果 TestMain 使用了 flags,這里應該加上flag.Parse()
	retCode := m.Run()                         // 執行測試
	fmt.Println("write teardown code here...") // 測試之后做一些拆卸工作
	os.Exit(retCode)                           // 退出測試
}

需要注意的是:在調用TestMain時, flag.Parse並沒有被調用。所以如果TestMain 依賴於command-line標志 (包括 testing 包的標記), 則應該顯示的調用flag.Parse

十六、子測試的Setup與Teardown

有時候我們可能需要為每個測試集設置Setup與Teardown,也有可能需要為每個子測試設置Setup與Teardown。下面我們定義兩個函數工具函數如下:

// 測試集的Setup與Teardown
func setupTestCase(t *testing.T) func(t *testing.T) {
	t.Log("如有需要在此執行:測試之前的setup")
	return func(t *testing.T) {
		t.Log("如有需要在此執行:測試之后的teardown")
	}
}

// 子測試的Setup與Teardown
func setupSubTest(t *testing.T) func(t *testing.T) {
	t.Log("如有需要在此執行:子測試之前的setup")
	return func(t *testing.T) {
		t.Log("如有需要在此執行:子測試之后的teardown")
	}
}

使用方式如下:

func TestSplit(t *testing.T) {
	type test struct { // 定義test結構體
		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) // 測試之前執行setup操作
	defer teardownTestCase(t)            // 測試之后執行testdoen操作

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) { // 使用t.Run()執行子測試
			teardownSubTest := setupSubTest(t) // 子測試之前執行setup操作
			defer teardownSubTest(t)           // 測試之后執行testdoen操作
			got := Split(tc.input, tc.sep)
			if !reflect.DeepEqual(got, tc.want) {
				t.Errorf("excepted:%#v, got:%#v", tc.want, got)
			}
		})
	}
}

測試結果如下:

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: 如有需要在此執行:測試之前的setup
    --- PASS: TestSplit/simple (0.00s)
        split_test.go:79: 如有需要在此執行:子測試之前的setup
        split_test.go:81: 如有需要在此執行:子測試之后的teardown
    --- PASS: TestSplit/wrong_sep (0.00s)
        split_test.go:79: 如有需要在此執行:子測試之前的setup
        split_test.go:81: 如有需要在此執行:子測試之后的teardown
    --- PASS: TestSplit/more_sep (0.00s)
        split_test.go:79: 如有需要在此執行:子測試之前的setup
        split_test.go:81: 如有需要在此執行:子測試之后的teardown
    --- PASS: TestSplit/leading_sep (0.00s)
        split_test.go:79: 如有需要在此執行:子測試之前的setup
        split_test.go:81: 如有需要在此執行:子測試之后的teardown
    split_test.go:73: 如有需要在此執行:測試之后的teardown
=== RUN   ExampleSplit
--- PASS: ExampleSplit (0.00s)
PASS
ok      github.com/Q1mi/studygo/code_demo/test_demo/split       0.006s

十七、示例函數

十八、示例函數的格式

go test特殊對待的第三種函數就是示例函數,它們的函數名以Example為前綴。它們既沒有參數也沒有返回值。標准格式如下:

func ExampleName() {
    // ...
}

十九、示例函數示例

下面的代碼是我們為Split函數編寫的一個示例函數:

func ExampleSplit() {
	fmt.Println(split.Split("a:b:c", ":"))
	fmt.Println(split.Split("沙河有沙又有河", "沙"))
	// Output:
	// [a b c]
	// [ 河有 又有河]
}

為你的代碼編寫示例代碼有如下三個用處:

  1. 示例函數能夠作為文檔直接使用,例如基於web的godoc中能把示例函數與對應的函數或包相關聯。

  2. 示例函數只要包含了// Output:也是可以通過go test運行的可執行測試。

    split $ go test -run Example
    PASS
    ok      github.com/Q1mi/studygo/code_demo/test_demo/split       0.006s
    ```</li>
    
    <li><p>示例函數提供了可以直接運行的示例代碼,可以直接在<code>golang.org</code>的<code>godoc</code>文檔服務器上使用<code>Go Playground</code>運行示例代碼。下圖為<code>strings.ToUpper</code>函數在Playground的示例函數效果。
    <img src="http://www.chenyoude.com/go/example.png?x-oss-process=style/watermark" alt="example.png"/></p></li>
    </ol>
    
    # 二十、練習題
    
    
    <ol>
    <li>編寫一個回文檢測函數,並為其編寫單元測試和基准測試,根據測試的結果逐步對其進行優化。(回文:一個字符串正序和逆序一樣,如“Madam,I&rsquo;mAdam”、“油燈少燈油”等。)</li>
    </ol>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM