gopl-zh.github.com/ch11/ch11-02-5.md

3.7 KiB
Raw Blame History

11.2.5. 編寫有效的測試

許多Go語言新人會驚異於它的極簡的測試框架。很多其它語言的測試框架都提供了識别測試函數的機製通常使用反射或元數據通過設置一些“setup”和“teardown”的鉤子函數來執行測試用例運行的初始化和之後的清理操作同時測試工具箱還提供了很多類似assert斷言值比較函數格式化輸出錯誤信息和停止一個識别的測試等輔助函數通常使用異常機製。雖然這些機製可以使得測試非常簡潔但是測試輸出的日誌卻會像火星文一般難以理解。此外雖然測試最終也會輸出PASS或FAIL的報告但是它們提供的信息格式卻非常不利於代碼維護者快速定位問題因爲失敗的信息的具體含義是非常隱晦的比如“assert: 0 == 1”或成頁的海量跟蹤日誌。

Go語言的測試風格則形成鮮明對比。它期望測試者自己完成大部分的工作定義函數避免重複就像普通編程那樣。編寫測試併不是一個機械的填空過程一個測試也有自己的接口盡管它的維護者也是測試僅有的一個用戶。一個好的測試不應該引發其他無關的錯誤信息它隻要清晰簡潔地描述問題的癥狀卽可有時候可能還需要一些上下文信息。在理想情況下維護者可以在不看代碼的情況下就能根據錯誤信息定位錯誤産生的原因。一個好的測試不應該在遇到一點小錯誤時就立刻退出測試它應該嚐試報告更多的相關的錯誤信息因爲我們可能從多個失敗測試的模式中發現錯誤産生的規律。

下面的斷言函數比較兩個值,然後生成一個通用的錯誤信息,併停止程序。它很方便使用也確實有效果,但是當測試失敗的時候,打印的錯誤信息卻幾乎是沒有價值的。它併沒有爲快速解決問題提供一個很好的入口。

import (
	"fmt"
	"strings"
	"testing"
)
// A poor assertion function.
func assertEqual(x, y int) {
	if x != y {
		panic(fmt.Sprintf("%d != %d", x, y))
	}
}
func TestSplit(t *testing.T) {
	words := strings.Split("a:b:c", ":")
	assertEqual(len(words), 3)
	// ...
}

從這個意義上説,斷言函數犯了過早抽象的錯誤:僅僅測試兩個整數是否相同,而放棄了根據上下文提供更有意義的錯誤信息的做法。我們可以根據具體的錯誤打印一個更有價值的錯誤信息,就像下面例子那樣。測試在隻有一次重複的模式出現時引入抽象。

func TestSplit(t *testing.T) {
	s, sep := "a:b:c", ":"
	words := strings.Split(s, sep)
	if got, want := len(words), 3; got != want {
		t.Errorf("Split(%q, %q) returned %d words, want %d",
			s, sep, got, want)
	}
	// ...
}

現在的測試不僅報告了調用的具體函數、它的輸入和結果的意義併且打印的眞實返迴的值和期望返迴的值併且卽使斷言失敗依然會繼續嚐試運行更多的測試。一旦我們寫了這樣結構的測試下一步自然不是用更多的if語句來擴展測試用例我們可以用像IsPalindrome的表驅動測試那樣來準備更多的s和sep測試用例。

前面的例子併不需要額外的輔助函數如果有可以使測試代碼更簡單的方法我們也樂意接受。我們將在13.3節看到一個類似reflect.DeepEqual輔助函數。開始一個好的測試的關鍵是通過實現你眞正想要的具體行爲然後才是考慮然後簡化測試代碼。最好的接口是直接從庫的抽象接口開始針對公共接口編寫一些測試函數。

練習11.5: 用表格驅動的技術擴展TestSplit測試併打印期望的輸出結果。