- Go語言精進之路:從新手到高手的編程思想、方法和技巧(2)
- 白明
- 1520字
- 2022-01-04 17:42:21
42.4 表驅動測試實踐中的注意事項
1. 表的實現方式
在上面的示例中,測試中使用的表是用自定義結構體的切片實現的,表也可以使用基于自定義結構體的其他集合類型(如map)來實現。我們將上面的例子改造為采用map來實現測試數據表:
// chapter8/sources/table_driven_strings_with_map_test.go func TestCompare(t *testing.T) { compareTests := map[string]struct { a, b string i int }{ `compareTwoEmptyString`: {"", "", 0}, `compareSecondParamIsEmpty`: {"a", "", 1}, `compareFirstParamIsEmpty`: {"", "a", -1}, } for name, tt := range compareTests { t.Run(name, func(t *testing.T) { cmp := strings.Compare(tt.a, tt.b) if cmp != tt.i { t.Errorf(`want %v, but Compare(%q, %q) = %v`, tt.i, tt.a, tt.b, cmp) } }) } }
不過使用map作為數據表時要注意,表內數據項的測試先后順序是不確定的。
執行兩次上面的示例,得到下面的不同結果:
// 第一次 $go test -v table_driven_strings_with_map_test.go === RUN TestCompare === RUN TestCompare/compareTwoEmptyString === RUN TestCompare/compareSecondParamIsEmpty === RUN TestCompare/compareFirstParamIsEmpty --- PASS: TestCompare (0.00s) --- PASS: TestCompare/compareTwoEmptyString (0.00s) --- PASS: TestCompare/compareSecondParamIsEmpty (0.00s) --- PASS: TestCompare/compareFirstParamIsEmpty (0.00s) PASS ok command-line-arguments 0.005s // 第二次 $go test -v table_driven_strings_with_map_test.go === RUN TestCompare === RUN TestCompare/compareFirstParamIsEmpty === RUN TestCompare/compareTwoEmptyString === RUN TestCompare/compareSecondParamIsEmpty --- PASS: TestCompare (0.00s) --- PASS: TestCompare/compareFirstParamIsEmpty (0.00s) --- PASS: TestCompare/compareTwoEmptyString (0.00s) --- PASS: TestCompare/compareSecondParamIsEmpty (0.00s) PASS ok command-line-arguments 0.005s
在上面兩次測試執行的輸出結果中,子測試的執行先后次序是不確定的,這是由map類型的自身性質所決定的:對map集合類型進行迭代所返回的集合中的元素順序是不確定的。
2. 測試失敗時的數據項的定位
對于非表驅動的測試,在測試失敗時,我們往往通過失敗點所在的行數即可判定究竟是哪塊測試代碼未通過:
$go test -v non_table_driven_strings_test.go === RUN TestCompare TestCompare: non_table_driven_strings_test.go:16: want 1, but Compare("", "") = 0 --- FAIL: TestCompare (0.00s) FAIL FAIL command-line-arguments 0.005s FAIL
在上面這個測試失敗的輸出結果中,我們可以直接通過行數(non_table_driven_strings_test.go的第16行)定位問題。但在表驅動的測試中,由于一般情況下表驅動的測試的測試結果成功與否的判定邏輯是共享的,因此再通過行數來定位問題就不可行了,因為無論是表中哪一項導致的測試失敗,失敗結果中輸出的引發錯誤的行號都是相同的:
$go test -v table_driven_strings_test.go === RUN TestCompare TestCompare: table_driven_strings_test.go:21: want -1, but Compare("", "") = 0 TestCompare: table_driven_strings_test.go:21: want 6, but Compare("a", "") = 1 --- FAIL: TestCompare (0.00s) FAIL FAIL command-line-arguments 0.005s FAIL
在上面這個測試失敗的輸出結果中,兩個測試失敗的輸出結果中的行號都是21,這樣我們就無法快速定位表中導致測試失敗的“元兇”。因此,為了在表測試驅動的測試中快速從輸出的結果中定位導致測試失敗的表項,我們需要在測試失敗的輸出結果中輸出數據表項的唯一標識。
最簡單的方法是通過輸出數據表項在數據表中的偏移量來輔助定位“元兇”:
// chapter8/sources/table_driven_strings_by_offset_test.go func TestCompare(t *testing.T) { compareTests := []struct { a, b string i int }{ {"", "", 7}, {"a", "", 6}, {"", "a", -1}, } for i, tt := range compareTests { cmp := strings.Compare(tt.a, tt.b) if cmp != tt.i { t.Errorf(`[table offset: %v] want %v, but Compare(%q, %q) = %v`, i+1, tt.i, tt.a, tt.b, cmp) } } }
運行該示例:
$go test -v table_driven_strings_by_offset_test.go === RUN TestCompare TestCompare: table_driven_strings_by_offset_test.go:21: [table offset: 1] want 7, but Compare("", "") = 0 TestCompare: table_driven_strings_by_offset_test.go:21: [table offset: 2] want 6, but Compare("a", "") = 1 --- FAIL: TestCompare (0.00s) FAIL FAIL command-line-arguments 0.005s FAIL
在上面這個例子中,我們通過在測試結果輸出中增加數據項在表中的偏移信息來快速定位問題數據。由于切片的數據項下標從0開始,這里進行了+1處理。
另一個更直觀的方式是使用名字來區分不同的數據項:
// chapter8/sources/table_driven_strings_by_name_test.go func TestCompare(t *testing.T) { compareTests := []struct { name, a, b string i int }{ {"compareTwoEmptyString", "", "", 7}, {"compareSecondStringEmpty", "a", "", 6}, {"compareFirstStringEmpty", "", "a", -1}, } for _, tt := range compareTests { cmp := strings.Compare(tt.a, tt.b) if cmp != tt.i { t.Errorf(`[%s] want %v, but Compare(%q, %q) = %v`, tt.name, tt.i, tt.a, tt.b, cmp) } } }
運行該示例:
$go test -v table_driven_strings_by_name_test.go === RUN TestCompare TestCompare: table_driven_strings_by_name_test.go:21: [compareTwoEmptyString] want 7, but Compare("", "") = 0 TestCompare: table_driven_strings_by_name_test.go:21: [compareSecondStringEmpty] want 6, but Compare("a", "") = 1 --- FAIL: TestCompare (0.00s) FAIL FAIL command-line-arguments 0.005s FAIL
在上面這個例子中,我們通過在自定義結構體中添加一個name字段來區分不同數據項,并在測試結果輸出該name字段以在測試失敗時輔助快速定位問題數據。
3. Errorf還是Fatalf
一般情況下,在表驅動的測試中,數據表中的所有表項共享同一個測試結果的判定邏輯。這樣我們需要在Errorf和Fatalf中選擇一個來作為測試失敗信息的輸出途徑。前面提到過Errorf不會中斷當前的goroutine的執行,即便某個數據項導致了測試失敗,測試依舊會繼續執行下去,而Fatalf恰好相反,它會終止測試執行。
至于是選擇Errorf還是Fatalf并沒有固定標準,一般而言,如果一個數據項導致的測試失敗不會對后續數據項的測試結果造成影響,那么推薦Errorf,這樣可以通過執行一次測試看到所有導致測試失敗的數據項;否則,如果數據項導致的測試失敗會直接影響到后續數據項的測試結果,那么可以使用Fatalf讓測試盡快結束,因為繼續執行的測試的意義已經不大了。
小結
在本條中,我們學習了編寫Go測試代碼的一般邏輯,并給出了編寫Go測試代碼的最佳實踐——基于表驅動測試,以及這種慣例的優點。最后我們了解了實施表驅動測試時需要注意的一些事項。