Go Wiki: Go 測試註釋
此頁面是對Go 程式碼審查註釋的補充,但專門針對測試程式碼。
在編輯此頁面之前,請先討論更改,即使是小的更改。很多人都有自己的看法,這裡不適合進行編輯戰。
- 斷言庫
- 選擇易於人類閱讀的子測試名稱
- 比較穩定結果
- 比較完整結構
- 相等比較和差異
- 先 Got 後 Want
- 標識函式
- 標識輸入
- 繼續進行
- 標記測試助手
- 列印差異
- 表驅動測試與多個測試函式
- 測試錯誤語義
斷言庫
避免使用“assert”庫來輔助你的測試。來自 xUnit 框架的 Go 開發者經常想寫如下程式碼:
assert.IsNotNil(t, "obj", obj)
assert.StringEq(t, "obj.Type", obj.Type, "blogPost")
assert.IntEq(t, "obj.Comments", obj.Comments, 2)
assert.StringNotEq(t, "obj.Body", obj.Body, "")
但這要麼會提前停止測試(如果 assert 呼叫了 t.Fatalf 或 panic),要麼會遺漏有關測試正確之處的有趣資訊。它還迫使 assert 包建立一整套新的子語言,而不是重用現有的程式語言(Go 本身)。Go 對列印結構體提供了良好的支援,所以一個更好的方法是這樣寫程式碼:
if obj == nil || obj.Type != "blogPost" || obj.Comments != 2 || obj.Body == "" {
t.Errorf("AddPost() = %+v", obj)
}
斷言庫使得編寫不精確的測試過於容易,並不可避免地會重複實現語言中已有的功能,如表示式求值、比較,有時甚至更多。努力編寫精確的測試,既能說明哪裡出錯了,也能說明哪裡是對的,並利用 Go 本身,而不是在 Go 中建立一種迷你語言。
選擇易於人類閱讀的子測試名稱
當你使用 t.Run 建立子測試時,第一個引數用於為測試提供描述性名稱。為了確保測試結果對閱讀日誌的人來說是易讀的,請選擇在轉義後仍然有用且可讀的子測試名稱。(測試執行器會將空格替換為下劃線,並轉義非列印字元)。
要標識輸入,請在子測試的函式體內使用 t.Log,或將其包含在測試的失敗訊息中,這樣它們就不會被測試執行器轉義。
比較完整結構
如果你的函式返回一個結構體,不要編寫對結構體中每個欄位進行單獨比較的測試程式碼。相反,構造一個你期望函式返回的結構體,然後一次性進行比較,使用差異或深度比較。相同的規則也適用於陣列和 map。
如果你的結構體需要進行近似相等性或其他型別的語義相等性比較,或者它包含無法進行相等性比較的欄位(例如,如果其中一個欄位是 io.Reader),那麼調整 cmp.Diff 或 cmp.Equal 比較,並使用 cmpopts 選項,例如 cmpopts.IgnoreInterfaces,可能就能滿足你的需求(示例);否則,這種技術就無法實現,請使用任何有效的方法。
如果你的函式返回多個返回值,你不需要在比較之前將它們包裝成一個結構體。只需單獨比較返回值並列印它們。
比較穩定結果
避免比較可能固有地依賴於你無法控制的外部包的輸出穩定性。相反,測試應該比較穩定的、對依賴項更改具有抵抗力的語義相關資訊。對於返回格式化字串或序列化位元組的功能,通常不能假定輸出是穩定的。
例如, json.Marshal 不保證它可能發出的確切位元組。它有自由(並且過去也曾)更改輸出。進行字串相等性比較以匹配確切 JSON 字串的測試,在 json 包更改其序列化位元組的方式時可能會失敗。相反,一個更健壯的測試將解析 JSON 字串的內容,並確保它在語義上等同於某個預期的資料結構。
相等比較和差異
== 運算子使用語言定義的比較來評估相等性。它可以比較的值包括數字、字串和指標值,以及包含這些值欄位的結構體。特別是,它僅當兩個指標指向同一個變數時才認為它們相等。
使用 cmp 包。使用 cmp.Equal 進行相等比較,使用 cmp.Diff 獲取物件之間易於人類閱讀的差異。
雖然 cmp 包不是 Go 標準庫的一部分,但它由 Go 團隊維護,並且應該在 Go 版本更新之間產生穩定的結果。它是使用者可配置的,應該能滿足大多數比較需求。
你會發現舊程式碼使用標準的 reflect.DeepEqual 函式來比較複雜結構。對於新程式碼,優先使用 cmp,並在實際可行的情況下考慮更新舊程式碼以使用 cmp。reflect.DeepEqual 對未匯出欄位和其他實現細節的變化很敏感。
注意:cmp 包也可以與協議緩衝區訊息一起使用,方法是在比較協議緩衝區訊息時包含 cmp.Comparer(proto.Equal) 選項。
先 Got 後 Want
測試輸出應先輸出函式返回的實際值,然後再列印期望值。列印測試輸出的常用格式是“YourFunc(%v) = %v, want %v”。
對於差異,方向性不太明顯,因此包含一個鍵來幫助解釋失敗非常重要。參見列印差異。
無論你在失敗訊息中使用哪種順序,都應該在失敗訊息中明確指明順序,因為現有程式碼在這個順序上並不一致。
標識函式
在大多數測試中,失敗訊息應包含失敗函式的名稱,即使從測試函式的名稱中看起來很明顯。
優先
t.Errorf("YourFunc(%v) = %v, want %v", in, got, want)
而不是
t.Errorf("got %v, want %v", got, want)
標識輸入
在大多數測試中,你的測試失敗訊息應該包含函式輸入(如果它們很簡短)。如果輸入的關鍵屬性不明顯(例如,因為輸入很大或不透明),你應該用描述所測試內容的名稱來命名你的測試用例,並在錯誤訊息中列印該描述。
不要使用測試表中測試的索引來代替命名你的測試或列印輸入。沒有人願意檢視你的測試表並數數才能弄清楚哪個測試用例失敗了。
繼續進行
即使在測試用例遇到失敗後,它們也應該儘可能地繼續進行,以便在一次執行中打印出所有失敗的檢查。這樣,修復失敗測試的人就不必玩“打地鼠”遊戲,修復一個錯誤,然後重新執行測試來查詢下一個錯誤。
從實際角度來看,優先呼叫 t.Error 而不是 t.Fatal。在比較函式輸出的幾個不同屬性時,對每個比較都使用 t.Error。
t.Fatal 通常只適用於一些測試設定失敗,沒有這些設定就無法執行測試。在表驅動測試中,t.Fatal 適用於在測試迴圈之前設定整個測試的功能的失敗。影響測試表中單個條目的失敗,使得無法繼續處理該條目,應按如下方式報告:
- 如果你不使用
t.Run子測試,你應該呼叫t.Error,然後跟一個continue語句來繼續處理下一個表條目。 - 如果你正在使用子測試(並且你是在呼叫
t.Run的內部),那麼t.Fatal會結束當前子測試並允許你的測試用例繼續到下一個子測試,所以請使用t.Fatal。
標記測試助手
測試助手是一個執行設定或拆卸任務的函式,例如構造輸入訊息,而該任務不依賴於被測試的程式碼。
如果你傳遞一個 *testing.T,請呼叫 t.Helper 來將測試助手中的失敗歸因於呼叫助手的行。
func TestSomeFunction(t *testing.T) {
golden := readFile(t, "testdata/golden.txt")
// ...
}
func readFile(t *testing.T, filename string) string {
t.Helper()
contents, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatal(err)
}
return string(contents)
}
當它會模糊測試失敗與導致失敗的條件之間的聯絡時,不要使用此模式。特別是,t.Helper 不應用於實現斷言庫。
列印差異
如果你的函式返回大型輸出,那麼閱讀失敗訊息的人可能很難在測試失敗時找到差異。不要同時列印返回值和期望值,而是製作一個差異。
在失敗訊息中新增一些文字來解釋差異的方向。
當你使用 cmp 包時(如果你將 (want, got) 傳遞給函式),類似“diff -want +got”的內容是很好的,因為你在格式字串中新增的 - 和 + 將與差異行開頭實際出現的 + 和 - 匹配。
差異會跨越多行,所以你應該在列印差異之前列印一個換行符。
表驅動測試與多個測試函式
表驅動測試應該在許多不同的測試用例可以使用類似的測試邏輯來測試時使用,例如在測試函式的實際輸出是否等於預期輸出時示例,或者在測試函式的輸出總是符合同一組不變數時。
當某些測試用例需要使用與其他測試用例不同的邏輯進行檢查時,編寫多個測試函式更為合適。當表中的每個條目都需要經過多種條件邏輯來為正確的輸入執行正確的輸出檢查時,你的測試程式碼的邏輯會變得難以理解。如果它們具有不同的邏輯但設定相同,那麼在單個測試函式中使用一系列子測試也可能是合理的。
你可以將表驅動測試與多個測試函式結合起來。例如,如果你正在測試一個函式的非錯誤輸出是否與預期輸出完全匹配,並且你還在測試該函式在輸入無效時返回某個非 nil 錯誤,那麼透過編寫兩個單獨的表驅動測試函式——一個用於正常非錯誤輸出,一個用於錯誤輸出——可以實現最清晰的單元測試。
測試錯誤語義
當單元測試執行字串比較或使用 reflect.DeepEqual 來檢查是否為特定輸入返回了特定型別的錯誤時,如果你將來必須重寫任何錯誤訊息,你可能會發現你的測試很脆弱。由於這有可能將你的單元測試變成一個變更檢測器,所以不要使用字串比較來檢查你的函式返回的錯誤型別。
使用字串比較來檢查被測包中的錯誤訊息是否滿足某些屬性是可以的,例如,它是否包含引數名稱。
如果你關心測試函式返回的確切錯誤型別,你應該將用於人類閱讀的錯誤字串與為程式化使用而公開的結構體分開。在這種情況下,你應該避免使用 fmt.Errorf,它傾向於破壞語義錯誤資訊。
許多編寫 API 的人並不確切地關心他們的 API 對不同輸入返回哪種型別的錯誤。如果你的 API 是這樣的,那麼使用 fmt.Errorf 建立錯誤訊息就足夠了,然後在單元測試中,只測試在你期望錯誤時錯誤是否為非 nil。
此內容是 Go Wiki 的一部分。