這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
目錄 [−]
- 測試
- 測試比較工具
- TDT (Table Driven Tests)
- 測試覆蓋率
- 效能測試
- Example
本章介紹Go語言的代碼的測試、效能測試以及樣本的寫法。
測試
雖然也有一些第三方的基於某種概念開發的測試庫,但是我覺得最好用還是官方的測試庫: testing。
常規地,我們會把測試代碼的檔案和正式的代碼檔案放在同一個檔案夾下,但是包含測試代碼的檔案應該以"_test.go"結尾。
測試檔案的包名可以和正式檔案的包名相同,也可以不同。比如正式的報名為abc,測試的報名就可以是abc_test,但是不能是其它的,比如xyz。
這兩種風格官方庫中都有。一般來說和正式的包名相同的話,我們就可以進行白盒測試,可以直接調用包下的未匯出的方法,包名不同則進行黑箱測試。根據 "The Go programming language"一書的介紹,這種方案還可以避免循環相依性的問題。
測試的檔案名稱不能以底線或者.開始,這些檔案不會被go test包含進來。
測試的方法名有特殊的定義,以"Test"開頭,並且參數為*testing.T:
1 |
func TestXxx(*testing.T) |
Xxx可以是任意的alphanumeric字串,但是首字母X不能是[a-z]中的字元,Testxyz就不是一個測試函數,但是Test123就是。
很多語言比如Java中的Junit、testng都提供了assertion輔助函數,可以方便的判定測試結果是否和期望的結果是否一致,但是Go官方並沒有提供,而且是有意為之,說是避免讓程式員犯懶。有地方庫提供了相應的功能,比如testify/assert。
如果測試結果不是你所期望的,你可以調用Fail、Error等方法觸發失敗訊號。
正常編譯的時候測試檔案會被排除在外,但是調用go test測試的時候會包含進來。
通過Skip方法可以掉過測試:
123456 |
func TestTimeConsuming(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } ...} |
完整的測試命令如下:
go test [build/test flags] [packages] [build/test flags & test binary flags]
不帶任何參數的時候,它會編譯和測試包下的所有的源檔案。
除了build flag,test還會額外的處理幾個編譯flag: -args、-c、-exec xprog、-i、-o file。
packages可以是絕對路徑、相對路徑(.或..)或者只是一個包名(go會在GOPATH環境變數的列表中尋找DIR/src/P,假設DIR在環境變數定義的檔案清單中, P為包名)。...可以模糊比對字串,比如x/...匹配x及x的子檔案夾。 go help packages會給出詳細的介紹。
build flag包含很多的flag,一般我們都不會加這些flag,如果你想瞭解,可以看官方文檔。
官方文檔的Description of testing flags描述了全部的測試flag,我們挑幾個常用的看一下。
-bench regexp:效能測試,支援運算式對測試函數進行篩選。-bench .則是對所有的benchmark函數測試
-benchmem:效能測試的時候顯示測試函數的記憶體配置的統計資訊
-count n:運行測試和效能多少此,預設一次
-run regexp:只運行特定的測試函數, 比如-run ABC只測試函數名中包含ABC的測試函數
-timeout t:測試時間如果超過t, panic,預設10分鐘
-v:顯示測試的詳細資料,也會把Log、Logf方法的日誌顯示出來
go test -v -args -x -v會編譯然後執行程式:pkg.test -test.v -x -v,這樣你就容易理解args參數的意義了。
Go 1.7中開始支援 sub-test的概念。
參考
- https://golang.org/pkg/testing/
- https://golang.org/doc/code.html#Testing
- https://golang.org/cmd/go/#hdr-Test_packages
- https://github.com/shageman/gotestit
測試比較工具
效能測試至關重要,你經常會問"A 更快還是 B更快",當然還的靠效能資料說話。當然效能測試並不是一件簡單的事情,今早我還看到陳皓寫的一篇批判Dubbo測試的一篇文章:效能測試應該怎麼做?。還好Go提供了一種容易的寫效能測試的方法,但是如何比較多個候選者之間的效能呢?
一種方式就是編寫多個測試函數,每個測試函數只測試一種候選方案,然後看測試的結果,比如我為Go序列化架構寫的效能測試:gosercomp。
本節要介紹的第一個工具就是 benchcmp,它可以比較兩個版本之間的效能的提升或者下降。比如你的程式碼程式庫在Go 1.6.2和Go 1.7編譯後的效能的改變:
123 |
go test -run=NONE -bench=. ./... > old.txt# make changesgo test -run=NONE -bench=. ./... > new.txt |
然後用這個工具進行比較:
123456789 |
$ benchcmp old.txt new.txtbenchmark old ns/op new ns/op deltaBenchmarkConcat 523 68.6 -86.88%benchmark old allocs new allocs deltaBenchmarkConcat 3 1 -66.67%benchmark old bytes new bytes deltaBenchmarkConcat 80 48 -40.00% |
第二個工具是prettybench,它可以將Go自己的效能的測試報告美化,更好讀:
PASSbenchmark iter time/iter--------- ---- ---------BenchmarkCSSEscaper 1000000 2843 ns/opBenchmarkCSSEscaperNoSpecials 5000000 671 ns/opBenchmarkDecodeCSS 1000000 1183 ns/opBenchmarkDecodeCSSNoSpecials 50000000 32 ns/opBenchmarkCSSValueFilter 5000000 501 ns/opBenchmarkCSSValueFilterOk 5000000 707 ns/opBenchmarkEscapedExecute 500000 6191 ns/opBenchmarkHTMLNospaceEscaper 1000000 2523 ns/opBenchmarkHTMLNospaceEscaperNoSpecials 5000000 596 ns/opBenchmarkStripTags 1000000 2351 ns/opBenchmarkStripTagsNoSpecials 10000000 260 ns/opBenchmarkJSValEscaperWithNum 1000000 1123 ns/opBenchmarkJSValEscaperWithStr 500000 4882 ns/opBenchmarkJSValEscaperWithStrNoSpecials 1000000 1461 ns/opBenchmarkJSValEscaperWithObj 500000 5052 ns/opBenchmarkJSValEscaperWithObjNoSpecials 1000000 1897 ns/opBenchmarkJSStrEscaperNoSpecials 5000000 608 ns/opBenchmarkJSStrEscaper 1000000 2633 ns/opBenchmarkJSRegexpEscaperNoSpecials 5000000 661 ns/opBenchmarkJSRegexpEscaper 1000000 2510 ns/opBenchmarkURLEscaper 500000 4424 ns/opBenchmarkURLEscaperNoSpecials 5000000 422 ns/opBenchmarkURLNormalizer 500000 3068 ns/opBenchmarkURLNormalizerNoSpecials 5000000 431 ns/opok html/template 62.874s
第三個要介紹的工具是benchviz,它使用benchcmp的結果,但是可以圖形化顯示效能的提升:
benchstat這個工具可以將多次測試的結果匯總,產生概要資訊。
參考:
- http://dominik.honnef.co/posts/2014/12/an_incomplete_list_of_go_tools/
TDT (Table Driven Tests)
TDT也叫表格驅動方法,有時也被歸為關鍵字驅動測試(keyword-driven testing,是針對自動化測試的軟體測試方法,它將建立測試程式的步驟分為規劃及實現二個階段。
Go官方庫中有些測試就使用了這種測試方法。
TDT中每個表格項就是一個完整的test case,包含輸入和期望的輸出,有時候還會加一些額外的資訊比如測試名稱。如果你發現你的測試中經常copy/paste操作,你就可以考慮把它們改造成TDT。
測試代碼就一塊,但是可以測試表格中的每一項。
下面是一個例子:
123456789101112131415161718192021222324252627 |
var flagtests = []struct { in string out string}{ {"%a", "[%a]"}, {"%-a", "[%-a]"}, {"%+a", "[%+a]"}, {"%#a", "[%#a]"}, {"% a", "[% a]"}, {"%0a", "[%0a]"}, {"%1.2a", "[%1.2a]"}, {"%-1.2a", "[%-1.2a]"}, {"%+1.2a", "[%+1.2a]"}, {"%-+1.2a", "[%+-1.2a]"}, {"%-+1.2abc", "[%+-1.2a]bc"}, {"%-1.2abc", "[%-1.2a]bc"},}func TestFlagParser(t *testing.T) { var flagprinter flagPrinter for _, tt := range flagtests { s := Sprintf(tt.in, &flagprinter) if s != tt.out { t.Errorf("Sprintf(%q, &flagprinter) => %q, want %q", tt.in, s, tt.out) } }} |
參考
- https://github.com/golang/go/wiki/TableDrivenTests
- http://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go
測試覆蓋率
從 Go 1.2開始, Go就提供了一個產生程式碼涵蓋範圍的工具cover。
程式碼涵蓋範圍描述了包中代碼有多少語句被測試所覆蓋。
比如代碼:
1234567891011121314151617 |
package sizefunc Size(a int) string { switch { case a < 0: return "negative" case a == 0: return "zero" case a < 10: return "small" case a < 100: return "big" case a < 1000: return "huge" } return "enormous"} |
測試代碼:
12345678910111213141516171819202122 |
package sizeimport "testing"type Test struct { in int out string}var tests = []Test{ {-1, "negative"}, {5, "small"},}func TestSize(t *testing.T) { for i, test := range tests { size := Size(test.in) if size != test.out { t.Errorf("#%d: Size(%d)=%s; want %s", i, test.in, size, test.out) } }} |
查看程式碼涵蓋範圍:
12345 |
% go test -coverPASScoverage: 42.9% of statementsok size 0.026s% |
想查看詳細的覆蓋率,可以產生coverage profile檔案:
12345 |
% go test -coverprofile=coverage.outPASScoverage: 42.9% of statementsok size 0.030s% |
產生html測試報告:
1 |
$ go tool cover -html=coverage.out |
一些網站可以協助你測試產生程式碼涵蓋範圍,你還可以把你的項目的badge寫在README檔案中, 比如gocover、coveralls
參考
- https://blog.golang.org/cover
效能測試
效能測試的寫法和單元測試的寫法類似,但是用"Benchmark"代替"Test"作為函數的開頭,而且函數的參數改為*testing.B:
1 |
func BenchmarkXxx(*testing.B) |
測試的時候,加上 -bench就可以執行效能的測試,如go test -bench .。
一個簡單的效能測試代碼如下:
12345 |
func BenchmarkHello(b *testing.B) { for i := 0; i < b.N; i++ { fmt.Sprintf("hello") }} |
測試代碼會執行b.N次,但是N會根據你的代碼的效能進行調整,代碼執行的快,N會大一些,代碼慢,N就小一些。
測試結果如下,執行了10000000次測試,每次測試花費282納秒:
1 |
BenchmarkHello 10000000 282 ns/op |
如果測試之前你需要準備一些花費時間較長的工作,你可以調用ResetTimer指定測試開始的時機:
1234567 |
func BenchmarkBigLen(b *testing.B) { big := NewBig() b.ResetTimer() for i := 0; i < b.N; i++ { big.Len() }} |
如果需要並行地執行測試,可以在測試的時候加上-cpu參數,可以執行RunParallel輔助方法:
12345678910 |
func BenchmarkTemplateParallel(b *testing.B) { templ := template.Must(template.New("test").Parse("Hello, { {.} }!")) b.RunParallel(func(pb *testing.PB) { var buf bytes.Buffer for pb.Next() { buf.Reset() templ.Execute(&buf, "World") } })} |
Example
一個程式碼範例函數就像一個測試函數一樣,但是它並不使用*testing.T作為參數報告錯誤或失敗,而是將輸出結果輸出到 os.Stdout 和 os.Stderr。
輸出結果會和函數內的Output:注釋中的結果比較, 這個注釋在函數體的最底部。如果沒有`Output:注釋,或者它的後面沒有文本,則代碼只會編譯,不會執行。
Godoc 可以顯示 ExampleXXX 的實現代碼, 用來示範函數XXX或者常量XXX或者變數XXX的使用。如果receiver為T或者*T的方法M,它的範例程式碼的命名方式應該是ExampleT_M。如果一個函數、常量或者變數有多可以在範例程式碼的方法名後加尾碼_xxx, xxx的第一個字元不能大寫。
舉個例子:
1234567891011 |
package ch11import "fmt"func ExampleAdd() {k := Add(1, 2)fmt.Println(k)// Output:// 3} |
相信你已經在godoc中看到了很多這樣的例子,你也應該為你的庫提供相應的例子,這樣別人很容易熟悉你的代碼。
你可以使用go help testfunc查看詳細說明。
範例程式碼的檔案名稱一般用example_test.go, 因為它們也是測試函數,所以檔案名稱要以"_test.go"結尾。
運行測試代碼:go test -v可以看到樣本函數也被測試了。
如果我們將上例注釋中的// 3改為// 2,運行go test -v可以看到出錯,因為執行結果和我們的輸出不一致。
12345678910111213 |
smallnestMBP:ch11 smallnest$ go test -v=== RUN Test123--- PASS: Test123 (0.00s)=== RUN ExampleAdd--- FAIL: ExampleAdd (0.00s)got:3want:2FAILexit status 1FAILgithub.com/smallnest/dive-into-go/ch110.005ssmallnestMBP:ch11 smallnest$ |
有時候,我們可能要為一組函數寫一個樣本,這個時候我們就需要一個whole file example,一個whole file example以"_test.go"結尾,只包含一個樣本函數,沒有測試函數或者效能測試函數,至少包含一個其它包層級的聲明,如下例就是一個完整的檔案:
123456789101112131415161718 |
bool { return a[i].Age < a[j].Age }func Example() { people := []Person{ {"Bob", 31}, {"John", 42}, {"Michael", 17}, {"Jenny", 26}, } fmt.Println(people) sort.Sort(ByAge(people)) fmt.Println(people) // Output: // [Bob: 31 John: 42 Michael: 17 Jenny: 26] // [Michael: 17 Jenny: 26 Bob: 31 John: 42]} |
參考
- https://blog.golang.org/examples