Go語言基礎之單元測試


目錄html

  • 1、go test工具
  • 2、測試函數
  • 3、測試函數的格式
  • 4、測試函數示例
  • 5、測試組
  • 6、子測試
  • 7、測試覆蓋率
  • 8、基準測試
  • 9、基準測試函數格式
  • 10、基準測試示例
  • 11、性能比較函數
  • 12、重置時間
  • 十3、並行測試
  • 十4、Setup與TearDown
  • 十5、TestMain
  • 十6、子測試的Setup與Teardown
  • 十7、示例函數
  • 十8、示例函數的格式
  • 十9、示例函數示例


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


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

1、go test工具

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

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

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

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

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

2、測試函數3、測試函數的格式

每一個測試函數必須導入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
4、測試函數示例

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

接下來,咱們定義一個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

這一次咱們的測試都經過了。

5、測試組

咱們如今還想要測試一下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
6、子測試

看起來都挺不錯的,可是若是測試用例比較多的時候,咱們是沒辦法一眼看出來具體是哪一個測試用例失敗了。咱們可能會想到下面的解決辦法:

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對應的子測試用例。

7、測試覆蓋率

測試覆蓋率是你的代碼被測試套件覆蓋的百分比。一般咱們使用的都是語句的覆蓋率,也就是在測試中至少被運行一次的代碼佔總代碼的比例。

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上圖中每一個用綠色標記的語句塊表示被覆蓋了,而紅色的表示沒有被覆蓋。

8、基準測試9、基準測試函數格式

基準測試就是在必定的工做負載之下檢測程序性能的一種方法。基準測試的基本格式以下:

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()
10、基準測試示例

咱們爲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的值,這個對於併發基準測試很重要。10000000和203ns/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的內存分配次數,而且減小了一半的內存分配。

11、性能比較函數

上面的基準測試只能獲得給定操做的絕對耗時,可是在不少性能問題是發生在兩個不一樣操做之間的相對耗時,好比同一個函數處理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)
}
12、重置時間

b.ResetTimer以前的處理不會放到執行時間裏,也不會輸出到報告中,因此能夠在以前作一些不計劃做爲測試報告的操做。例如:

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

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

RunParallel會建立出多個goroutine,並將b.N分配給這些goroutine執行, 其中goroutine數量的默認值爲GOMAXPROCS。用戶若是想要增長非CPU受限(non-CPU-bound)基準測試的並行性, 那麼能夠在RunParallel以前調用SetParallelism 。RunParallel一般會與-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數量。

十4、Setup與TearDown

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

十5、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。

十6、子測試的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
十7、示例函數十8、示例函數的格式

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

func ExampleName() {
    // ...
}
十9、示例函數示例

下面的代碼是咱們爲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>
    
    # 二10、練習題
    
    
    <ol>
    <li>編寫一個迴文檢測函數,併爲其編寫單元測試和基準測試,根據測試的結果逐步對其進行優化。(迴文:一個字符串正序和逆序同樣,如「Madam,I&rsquo;mAdam」、「油燈少燈油」等。)</li>
    </ol>
相關文章
相關標籤/搜索