## 11.2. 測試函數
每個測試函數必須導入testing包。測試函數有如下的簽名:
```Go
func TestName(t *testing.T) {
// ...
}
```
測試函數的名字必須以Test開頭,可選的后綴名必須以大寫字母開頭:
```Go
func TestSin(t *testing.T) { /* ... */ }
func TestCos(t *testing.T) { /* ... */ }
func TestLog(t *testing.T) { /* ... */ }
```
其中t參數用于報告測試失敗和附加的日志信息。讓我們定義一個實例包gopl.io/ch11/word1,其中只有一個函數IsPalindrome用于檢查一個字符串是否從前向后和從后向前讀都是一樣的。(下面這個實現對于一個字符串是否是回文字符串前后重復測試了兩次;我們稍后會再討論這個問題。)
<u><i>gopl.io/ch11/word1</i></u>
```Go
// Package word provides utilities for word games.
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測試文件中包含了TestPalindrome和TestNonPalindrome兩個測試函數。每一個都是測試IsPalindrome是否給出正確的結果,并使用t.Error報告失敗信息:
```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`)
}
}
```
`go test`命令如果沒有參數指定包那么將默認采用當前目錄對應的包(和`go build`命令一樣)。我們可以用下面的命令構建和運行測試。
```
$ cd $GOPATH/src/gopl.io/ch11/word1
$ go test
ok gopl.io/ch11/word1 0.008s
```
結果還比較滿意,我們運行了這個程序, 不過沒有提前退出是因為還沒有遇到BUG報告。不過一個法國名為“Noelle Eve Elleon”的用戶會抱怨IsPalindrome函數不能識別“été”。另外一個來自美國中部用戶的抱怨則是不能識別“A man, a plan, a canal: Panama.”。執行特殊和小的BUG報告為我們提供了新的更自然的測試用例。
```Go
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)
}
}
```
為了避免兩次輸入較長的字符串,我們使用了提供了有類似Printf格式化功能的 Errorf函數來匯報錯誤結果。
當添加了這兩個測試用例之后,`go test`返回了測試失敗的信息。
```
$ 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`可用于打印每個測試函數的名字和運行時間:
```
$ go test -v
=== RUN TestPalindrome
--- PASS: TestPalindrome (0.00s)
=== RUN TestNonPalindrome
--- PASS: TestNonPalindrome (0.00s)
=== RUN TestFrenchPalindrome
--- FAIL: TestFrenchPalindrome (0.00s)
word_test.go:28: IsPalindrome("été") = false
=== RUN TestCanalPalindrome
--- FAIL: TestCanalPalindrome (0.00s)
word_test.go:35: IsPalindrome("A man, a plan, a canal: Panama") = false
FAIL
exit status 1
FAIL gopl.io/ch11/word1 0.017s
```
參數`-run`對應一個正則表達式,只有測試函數名被它正確匹配的測試函數才會被`go test`測試命令運行:
```
$ go test -v -run="French|Canal"
=== RUN TestFrenchPalindrome
--- FAIL: TestFrenchPalindrome (0.00s)
word_test.go:28: IsPalindrome("été") = false
=== RUN TestCanalPalindrome
--- FAIL: TestCanalPalindrome (0.00s)
word_test.go:35: IsPalindrome("A man, a plan, a canal: Panama") = false
FAIL
exit status 1
FAIL gopl.io/ch11/word1 0.014s
```
當然,一旦我們已經修復了失敗的測試用例,在我們提交代碼更新之前,我們應該以不帶參數的`go test`命令運行全部的測試用例,以確保修復失敗測試的同時沒有引入新的問題。
我們現在的任務就是修復這些錯誤。簡要分析后發現第一個BUG的原因是我們采用了 byte而不是rune序列,所以像“été”中的é等非ASCII字符不能正確處理。第二個BUG是因為沒有忽略空格和字母的大小寫導致的。
針對上述兩個BUG,我們仔細重寫了函數:
<u><i>gopl.io/ch11/word2</i></u>
```Go
// Package word provides utilities for word games.
package word
import "unicode"
// IsPalindrome reports whether s reads the same forward and backward.
// Letter case is ignored, as are non-letters.
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
}
```
同時我們也將之前的所有測試數據合并到了一個測試中的表格中。
```Go
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 test gopl.io/ch11/word2
ok gopl.io/ch11/word2 0.015s
```
這種表格驅動的測試在Go語言中很常見。我們可以很容易地向表格添加新的測試數據,并且后面的測試邏輯也沒有冗余,這樣我們可以有更多的精力去完善錯誤信息。
失敗測試的輸出并不包括調用t.Errorf時刻的堆棧調用信息。和其他編程語言或測試框架的assert斷言不同,t.Errorf調用也沒有引起panic異常或停止測試的執行。即使表格中前面的數據導致了測試的失敗,表格后面的測試數據依然會運行測試,因此在一個測試中我們可能了解多個失敗的信息。
如果我們真的需要停止測試,或許是因為初始化失敗或可能是早先的錯誤導致了后續錯誤等原因,我們可以使用t.Fatal或t.Fatalf停止當前測試函數。它們必須在和測試函數同一個goroutine內調用。
測試失敗的信息一般的形式是“f(x) = y, want z”,其中f(x)解釋了失敗的操作和對應的輸入,y是實際的運行結果,z是期望的正確的結果。就像前面檢查回文字符串的例子,實際的函數用于f(x)部分。顯示x是表格驅動型測試中比較重要的部分,因為同一個斷言可能對應不同的表格項執行多次。要避免無用和冗余的信息。在測試類似IsPalindrome返回布爾類型的函數時,可以忽略并沒有額外信息的z部分。如果x、y或z是y的長度,輸出一個相關部分的簡明總結即可。測試的作者應該要努力幫助程序員診斷測試失敗的原因。
**練習 11.1:** 為4.3節中的charcount程序編寫測試。
**練習 11.2:** 為(§6.5)的IntSet編寫一組測試,用于檢查每個操作后的行為和基于內置map的集合等價,后面練習11.7將會用到。
{% include "./ch11-02-1.md" %}
{% include "./ch11-02-2.md" %}
{% include "./ch11-02-3.md" %}
{% include "./ch11-02-4.md" %}
{% include "./ch11-02-5.md" %}
{% include "./ch11-02-6.md" %}
- 前言
- Go語言起源
- Go語言項目
- 本書的組織
- 更多的信息
- 致謝
- 入門
- Hello, World
- 命令行參數
- 查找重復的行
- GIF動畫
- 獲取URL
- 并發獲取多個URL
- Web服務
- 本章要點
- 程序結構
- 命名
- 聲明
- 變量
- 賦值
- 類型
- 包和文件
- 作用域
- 基礎數據類型
- 整型
- 浮點數
- 復數
- 布爾型
- 字符串
- 常量
- 復合數據類型
- 數組
- Slice
- Map
- 結構體
- JSON
- 文本和HTML模板
- 函數
- 函數聲明
- 遞歸
- 多返回值
- 錯誤
- 函數值
- 匿名函數
- 可變參數
- Deferred函數
- Panic異常
- Recover捕獲異常
- 方法
- 方法聲明
- 基于指針對象的方法
- 通過嵌入結構體來擴展類型
- 方法值和方法表達式
- 示例: Bit數組
- 封裝
- 接口
- 接口是合約
- 接口類型
- 實現接口的條件
- flag.Value接口
- 接口值
- sort.Interface接口
- http.Handler接口
- error接口
- 示例: 表達式求值
- 類型斷言
- 基于類型斷言識別錯誤類型
- 通過類型斷言查詢接口
- 類型分支
- 示例: 基于標記的XML解碼
- 補充幾點
- Goroutines和Channels
- Goroutines
- 示例: 并發的Clock服務
- 示例: 并發的Echo服務
- Channels
- 并發的循環
- 示例: 并發的Web爬蟲
- 基于select的多路復用
- 并發的退出
- 示例: 聊天服務
- 基于共享變量的并發
- 競爭條件
- sync.Mutex互斥鎖
- sync.RWMutex讀寫鎖
- 內存同步
- 競爭條件檢測
- 示例: 并發的非阻塞緩存
- Goroutines和線程
- 包和工具
- 包簡介
- 導入路徑
- 包聲明
- 導入聲明
- 包的匿名導入
- 包和命名
- 工具
- 測試
- go test
- 測試函數
- 測試覆蓋率
- 基準測試
- 剖析
- 示例函數
- 反射
- 為何需要反射?
- reflect.Type和reflect.Value
- Display遞歸打印
- 示例: 編碼S表達式
- 通過reflect.Value修改值
- 示例: 解碼S表達式
- 顯示一個類型的方法集
- 幾點忠告
- 底層編程
- unsafe.Sizeof, Alignof 和 Offsetof
- unsafe.Pointer
- 示例: 深度相等判斷
- 通過cgo調用C代碼
- 幾點忠告
- 附錄
- 附錄A:原文勘誤
- 附錄B:作者譯者
- 附錄C:譯文授權
- 附錄D:其它語言