golang 測試
參考《Go語言聖經》P395
一、概述
go test命令是一個按照一定的約定和組織的測試程式碼的驅動程式。在包目錄內,所有以_test.go為字尾名的原始檔並不是go build構建包的一部分,它們是go test測試的一部分。在*_test.go檔案中,有三種類型的函式:測試函式、基準測試函式、示例函式。
-
1.一個測試函式是以Test為函式名字首的函式,用於測試程式的一些邏輯行為是否正確;go test命令會呼叫這些測試函式並報告測試結果是PASS或FAIL。
-
2.基準測試函式是以Benchmark為函式名字首的函式,它們用於衡量一些函式的效能;go test命令會多次執行基準函式以計算一個平均的執行時間。
-
3.示例函式是以Example為函式名字首的函式,提供一個由編譯器保證正確性的示例文件。
go test命令會遍歷所有的*_test.go檔案中符合上述命名規則的函式,然後生成一個臨時的main包用於呼叫相應的測試函式,然後構建並執行、報告測試結果,最後清理測試中生成的臨時檔案。
二、測試函式
每個測試函式必須匯入testing包。測試函式的名字必須以Test開頭,可選的字尾名必須以大寫字母開頭。
//word1.go package word // IsPalindrome reports whether s reads the same forward and backward. // (Our first attempt.) func IsPalindrome(s string) bool { for i := range s { if s[i] != s[len(s)-1-i] { return false } } return true } //word_test.go package word import "testing" func TestPalindrome(t *testing.T) { if !IsPalindrome("detartrated") { t.Error(`IsPalindrome("detartrated") = false`) } if !IsPalindrome("kayak") { t.Error(`IsPalindrome("kayak") = false`) } } func TestNonPalindrome(t *testing.T) { if IsPalindrome("palindrome") { t.Error(`IsPalindrome("palindrome") = true`) } } func TestFrenchPalindrome(t *testing.T) { if !IsPalindrome("été") { t.Error(`IsPalindrome("été") = false`) } } func TestCanalPalindrome(t *testing.T) { input := "A man, a plan, a canal: Panama" if !IsPalindrome(input) { t.Errorf(`IsPalindrome(%q) = false`, input) } }
測試命令如下
$ go test --- FAIL: TestFrenchPalindrome (0.00s) word_test.go:28: IsPalindrome("été") = false --- FAIL: TestCanalPalindrome (0.00s) word_test.go:35: IsPalindrome("A man, a plan, a canal: Panama") = false FAIL FAIL gopl.io/ch11/word1 0.014s
引數 -v 可用於列印每個測試函式的名字和執行時間。
引數 -run 對應一個正則表示式,只有測試函式名被它正確匹配的測試函式才會被 go test 測試命令執行:$ go test -v -run="French|Canal"
。不過我在Goland裡沒有設定好run引數,只能開命令列來運行了。
我們現在的任務就是修復這些錯誤。簡要分析後發現第一個BUG的原因是我們採用了 byte而
不是rune序列,所以像“été”中的é等非ASCII字元不能正確處理。第二個BUG是因為沒有忽略
空格和字母的大小寫導致的。
關於rune和byte區別,可以參考Golang 學習筆記三 字串 字典 ,這裡使用rune切片重新處理,並使用unicode.IsLetter過濾掉非字母,使用unicode.ToLower過濾大小寫。
func IsPalindrome(s string) bool { var letters []rune for _, r := range s { if unicode.IsLetter(r) { letters = append(letters, unicode.ToLower(r)) } } for i := range letters { if letters[i] != letters[len(letters)-1-i] { return false } } return true }
然後合併測試資料:
func TestIsPalindrome(t *testing.T) { var tests = []struct { input string want bool }{ {"", true}, {"a", true}, {"aa", true}, {"ab", false}, {"kayak", true}, {"detartrated", true}, {"A man, a plan, a canal: Panama", true}, {"Evil I did dwell; lewd did I live.", true}, {"Able was I ere I saw Elba", true}, {"été", true}, {"Et se resservir, ivresse reste.", true}, {"palindrome", false}, // non-palindrome {"desserts", false}, // semi-palindrome } for _, test := range tests { if got := IsPalindrome(test.input); got != test.want { t.Errorf("IsPalindrome(%q) = %v", test.input, got) } } }
這種表格驅動的測試在Go語言中很常見的。我們很容易向表格新增新的測試資料,並且後面的測試邏輯也沒有冗餘,這樣我們可以有更多的精力地完善錯誤資訊。
失敗測試的輸出並不包括呼叫t.Errorf時刻的堆疊呼叫資訊。和其他程式語言或測試框架的assert斷言不同,t.Errorf呼叫也沒有引起panic異常或停止測試的執行。即使表格中前面的資料導致了測試的失敗,表格後面的測試資料依然會執行測試,因此在一個測試中我們可能瞭解多個失敗的資訊。
如果我們真的需要停止測試,或許是因為初始化失敗或可能是早先的錯誤導致了後續錯誤等原因,我們可以使用t.Fatal或t.Fatalf停止當前測試函式。它們必須在和測試函式同一個goroutine內呼叫。
二、基準測試
基準測試是測量一個程式在固定工作負載下的效能。在Go語言中,基準測試函式和普通測試函式寫法類似,但是以Benchmark為字首名,並且帶有一個 *testing.B 型別的引數; *testing.B 引數除了提供和 *testing.T 類似的方法,還有額外一些和效能測量相關的方法。它還提供了一個整數N,用於指定操作執行的迴圈次數。
下面是IsPalindrome函式的基準測試,其中迴圈將執行N次。
import "testing" func BenchmarkIsPalindrome(b *testing.B) { for i := 0; i < b.N; i++ { IsPalindrome("A man, a plan, a canal: Panama") } }
我們用下面的命令執行基準測試。和普通測試不同的是,預設情況下不執行任何基準測試。我們需要通過 -bench 命令列標誌引數手工指定要執行的基準測試函式。該引數是一個正則表示式,用於匹配要執行的基準測試函式的名字,預設值是空的。其中“.”模式將可以匹配所有基準測試函式,但是這裡總共只有一個基準測試函式,因此和 -bench=IsPalindrome 引數是等價的效果。
$ cd $GOPATH/src/gopl.io/ch11/word2 $ go test -bench=. PASS BenchmarkIsPalindrome-8 1000000 1035 ns/op ok gopl.io/ch11/word2 2.179s
結果中基準測試名的數字字尾部分,這裡是8,表示執行時對應的GOMAXPROCS的值,這對於一些和併發相關的基準測試是重要的資訊。
報告顯示每次呼叫IsPalindrome函式花費1.035微秒,是執行1,000,000次的平均時間。因為基準測試驅動器開始時並不知道每個基準測試函式執行所花的時間,它會嘗試在真正執行基準測試前先嚐試用較小的N執行測試來估算基準測試函式所需要的時間,然後推斷一個較大的時間保證穩定的測量結果。
迴圈在基準測試函式內實現,而不是放在基準測試框架內實現,這樣可以讓每個基準測試函式有機會在迴圈啟動前執行初始化程式碼,這樣並不會顯著影響每次迭代的平均執行時間。如果還是擔心初始化程式碼部分對測量時間帶來干擾,那麼可以通過testing.B引數提供的方法來臨時關閉或重置計時器,不過這些一般很少會用到。(b.ResetTimer()
)
現在我們有了一個基準測試和普通測試,我們可以很容易測試新的讓程式執行更快的想法。也許最明顯的優化是在IsPalindrome函式中第二個迴圈的停止檢查,這樣可以避免每個比較都做兩次。不過很多情況下,一個明顯的優化並不一定就能程式碼預期的效果。這個改進在基準測試中只帶來了4%的效能提升。
另一個改進想法是在開始為每個字元預先分配一個足夠大的陣列,這樣就可以避免在append呼叫時可能會導致記憶體的多次重新分配。宣告一個letters陣列變數,並指定合適的大小,像下面這樣
letters := make([]rune, 0, len(s)) for _, r := range s { if unicode.IsLetter(r) { letters = append(letters, unicode.ToLower(r)) } }
這個改進提升效能約35%,報告結果是基於2,000,000次迭代的平均執行時間統計。如這個例子所示,快的程式往往是伴隨著較少的記憶體分配。 -benchmem 命令列標誌引數將在報告中包含記憶體的分配資料統計。我們可以比較優化前後記憶體的分配情況:
$ go test -bench=. -benchmem PASS BenchmarkIsPalindrome 1000000 1026 ns/op 304 B/op 4 allocs/op
這是優化之後的結果:
$ go test -bench=. -benchmem PASS BenchmarkIsPalindrome 2000000 807 ns/op 128 B/op 1 allocs/op
用一次記憶體分配代替多次的記憶體分配節省了75%的分配呼叫次數和減少近一半的記憶體需求。
三、示例函式
第三種 go test 特別處理的函式是示例函式,以Example為函式名開頭。示例函式沒有函式引數和返回值。下面是IsPalindrome函式對應的示例函式:
func ExampleIsPalindrome() { fmt.Println(IsPalindrome("A man, a plan, a canal: Panama")) fmt.Println(IsPalindrome("palindrome")) // Output: // true // false }
示例函式有三個用處。最主要的一個是作為文件:一個包的例子可以更簡潔直觀的方式來演示函式的用法,比文字描述更直接易懂,特別是作為一個提醒或快速參考時。一個示例函式方便展示屬於同一個介面的幾種型別或函式直接的關係,所有的文件都必須關聯到一個地方,就像一個型別或函式宣告都統一到包一樣。同時,示例函式和註釋並不一樣,示例函式是完整真實的Go程式碼,需要接受編譯器的編譯時檢查,這樣可以保證示例程式碼不會腐爛成不能使用的舊程式碼。
根據示例函式的字尾名部分,godoc的web文件會將一個示例函式關聯到某個具體函式或包本身,因此ExampleIsPalindrome示例函式將是IsPalindrome函式文件的一部分,Example示例函式將是包文件的一部分。
示例文件的第二個用處是在 go test 執行測試的時候也執行示例函式測試。如果示例函式內含有類似上面例子中的 // Output: 格式的註釋,那麼測試工具會執行這個示例函式,然後檢測這個示例函式的標準輸出和註釋是否匹配。
示例函式的第三個目的提供一個真實的演練場。http://golang.org 就是由godoc提供的文件服務,它使用了Go Playground提高的技術讓使用者可以在瀏覽器中線上編輯和執行每個示例函式,就像圖11.4所示的那樣。這通常是學習函式使用或Go語言特性最快捷的方式。
四、模仿呼叫--《Go語言實戰》第9章
不能總是假設執行測試的機器可以訪問網際網路。此外,依賴不屬於你的或者你無法操作的服務來進行測試,也不是一個好習慣。這兩點會嚴重影響測試持續整合和部署的自動化。如果突然斷網,導致測試失敗,就沒辦法部署新構建的程式。
為了修正這個問題,標準庫包含一個名為 httptest 的包,它讓開發人員可以模仿基於HTTP 的網路呼叫。模仿(mocking)是一個很常用的技術手段,用來在執行測試時模擬訪問不可用的資源。包 httptest 可以讓你能夠模仿網際網路資源的請求和響應。在我們的單元測試中,通過模仿 http.Get 的響應,我們可以解決在圖 9-4 中遇到的問題,保證在沒有網路的時候,我們的測試也不會失敗,依舊可以驗證我們的 http.Get 呼叫正常工作,並且可以處理預期的響應。讓我們看一下基礎單元測試,並將其改為模仿呼叫 goinggo.net 網站的 RSS 列表,如程式碼清單 9-12 所示。
程式碼清單 9-12 listing12_test.go:第 01 行到第 41 行 01 // 這個示例程式展示如何內部模仿 HTTP GET 呼叫 02 // 與本書之前的例子有些差別 03 package listing12 04 05 import ( 06"encoding/xml" 07"fmt" 08"net/http" 09"net/http/httptest" 10"testing" 11 ) 12 13 const checkMark = "\u2713" 14 const ballotX = "\u2717" 15 16 // feed 模仿了我們期望接收的 XML 文件 17 var feed = `<?xml version="1.0" encoding="UTF-8"?> 18 <rss> 19 <channel> 20<title>Going Go Programming</title> 21<description>Golang : https://github.com/goinggo</description> 22<link>http://www.goinggo.net/</link> 23<item> 24<pubDate>Sun, 15 Mar 2015 15:04:00 +0000</pubDate> 25<title>Object Oriented Programming Mechanics</title> 26<description>Go is an object oriented language.</description> 27<link>http://www.goinggo.net/2015/03/object-oriented</link> 28</item> 29 </channel> 30 </rss>` 31 32 // mockServer 返回用來處理請求的伺服器的指標 33 func mockServer() *httptest.Server { 34f := func(w http.ResponseWriter, r *http.Request) { 35w.WriteHeader(200) 36w.Header().Set("Content-Type", "application/xml") 37fmt.Fprintln(w, feed) 38} 39 40return httptest.NewServer(http.HandlerFunc(f)) 41 }