Go Wiki:Go 測試註解

此頁面是 Go 程式碼檢閱評論 的補充,但特別針對測試程式碼。

請在編輯此頁面之前 討論變更,即使是輕微的變更。許多人有意見,這裡不是編輯爭論的地方。

斷言函式庫

避免使用「斷言」函式庫來幫助您的測試。從 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, "")

但這會提早停止測試(如果斷言呼叫 t.Fatalfpanic),或省略測試正確的部分的有趣資訊。它也強迫斷言套件建立一個全新的子語言,而不是重複使用現有的程式語言(Go 本身)。Go 對列印結構有良好的支援,所以撰寫此程式碼的更好方法是

if obj == nil || obj.Type != "blogPost" || obj.Comments != 2 || obj.Body == "" {
    t.Errorf("AddPost() = %+v", obj)
}

斷言函式庫讓撰寫不精確的測試變得太容易,並且不可避免地複製語言中已有的功能,例如表達式評估、比較,有時甚至更多。努力撰寫關於錯誤和正確的部分都很精確的測試,並使用 Go 本身,而不是在 Go 內部建立一個迷你語言。

選擇人類可讀的子測試名稱

當您使用 t.Run 來建立子測試時,第一個引數用作測試的描述性名稱。為了確保測試結果對閱讀記錄檔的人類來說是可讀的,請選擇在跳脫後仍然有用且可讀的子測試名稱。(測試執行器會將空格替換為底線,並跳脫不可列印字元)。

若要識別輸入,請在子測試的主體中使用 t.Log,或將它們包含在測試的失敗訊息中,測試執行器不會跳脫這些訊息。

比較完整的結構

如果您的函式傳回結構,請勿撰寫執行個別比較結構中每個欄位的測試程式碼。請改為建構您預期函式傳回的結構,並使用差異深度比較一次比較。相同的規則適用於陣列和映射。

如果您的結構需要比較近似相等或其他類型的語義相等,或它包含無法比較相等的欄位(例如,如果其中一個欄位是 io.Reader),調整cmp.Diffcmp.Equal比較與cmpopts選項,例如cmpopts.IgnoreInterfaces,可能會滿足您的需求(範例);否則,此技術將無法使用,因此請執行任何可行的方法。

如果您的函式傳回多個傳回值,您不需要在比較它們之前將它們包裝在結構中。只要個別比較傳回值並列印它們即可。

比較穩定的結果

避免比較可能本質上依賴於您無法控制的某些外部套件的輸出穩定性的結果。相反地,測試應比較語義相關且穩定且能抵抗依賴項變更的資訊。對於傳回格式化字串或序列化位元的函式,通常無法假設輸出是穩定的。

例如,json.Marshal 對於它可能發出的確切位元組不提供任何保證。它有權利變更(並且過去已變更)輸出。如果 json 套件變更它序列化位元組的方式,則對確切 JSON 字串執行字串相等性的測試可能會中斷。相反地,更穩健的測試會剖析 JSON 字串的內容,並確保它在語意上等同於某些預期的資料結構。

相等性比較和差異

== 算子使用 語言定義的比較 來評估相等性。它可以比較的數值包括數字、字串和指標值,以及具有這些值欄位的結構。特別是,它僅在兩個指標指向同一個變數時才判斷它們相等。

使用 cmp 套件。使用 cmp.Equal 進行相等性比較,並使用 cmp.Diff 取得物件之間可供人類閱讀的差異。

儘管 cmp 套件不是 Go 標準函式庫的一部分,但它是由 Go 團隊維護的,並且應該可以在 Go 版本更新中產生穩定的結果。它可以由使用者設定,並且應該可以滿足大多數的比較需求。

您會發現較舊的程式碼使用標準的 reflect.DeepEqual 函式來比較複雜的結構。對於新的程式碼,請優先使用 cmp,並考慮在實務上更新較舊的程式碼以使用 cmpreflect.DeepEqual 對於未匯出的欄位變更和其他實作細節很敏感。

注意:cmp 套件也可以與通訊協定緩衝訊息一起使用,方法是在比較通訊協定緩衝訊息時包含 cmp.Comparer(proto.Equal) 選項。

在想要之前得到

測試輸出應該在列印預期的值之前輸出函式傳回的實際值。列印測試輸出的通常格式為「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 適用於在測試迴圈前設定整個測試函式的失敗。影響測試表格中單一項目的失敗,導致無法繼續執行該項目的,應報告如下

標記測試輔助程式

測試輔助函式是一個執行設定或清除工作(例如建構輸入訊息)的函式,且不依賴於受測程式碼。

如果您傳遞 *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 不應被用於實作斷言函式庫。

如果您的函式傳回大量輸出,那麼當您的測試失敗時,閱讀失敗訊息的人可能會難以找出差異。請勿列印傳回值和想要的數值,而是建立一個 diff。

在您的失敗訊息中新增一些文字,說明 diff 的方向。

當您使用 cmp 套件時(如果您將 (want, got) 傳遞給函式),類似「diff -want +got」的內容會很好,因為您新增到格式化字串的 -+ 會與實際出現在 diff 行開頭的 +- 相符。

diff 會跨越多行,因此您應該在列印 diff 之前列印一個換行符號。

表格驅動測試與多個測試函式

表格驅動 測試應該在可以使用類似測試邏輯測試許多不同測試案例時使用,例如在測試函數的實際輸出是否等於預期的輸出 範例,或在測試函數的輸出是否總是符合同一組不變數時使用。

當某些測試案例需要使用與其他測試案例不同的邏輯進行檢查時,撰寫多個測試函數會更適當。當表格中的每個項目都必須經過多種條件邏輯才能針對正確類型的輸入進行正確類型的輸出檢查時,測試程式碼的邏輯可能會難以理解。如果它們具有不同的邏輯但設定相同,則單一測試函數中的子測試順序也可能合理。

您可以將表格驅動測試與多個測試函數結合使用。例如,如果您要測試函數的非錯誤輸出是否與預期的輸出完全相符,並且還要測試函數在取得無效輸入時是否會傳回一些非 nil 錯誤,則最清楚的單元測試可以透過撰寫兩個獨立的表格驅動測試函數來達成,一個用於正常的非錯誤輸出,另一個用於錯誤輸出。

測試錯誤語意

當單元測試執行字串比較或使用 reflect.DeepEqual 來檢查特定類型的錯誤是否已針對特定輸入傳回時,您可能會發現如果未來必須重新表述其中任何錯誤訊息,您的測試會很脆弱。由於這可能會將您的單元測試變成 變更偵測器,因此請勿使用字串比較來檢查函數傳回的錯誤類型。

使用字串比較來檢查來自受測套件的錯誤訊息是否滿足某些屬性是可以的,例如它包含參數名稱。

如果您關心測試函式傳回的錯誤類型,您應該將供人閱讀的錯誤字串與供程式設計使用公開的結構分開。在此情況下,您應該避免使用 `fmt.Errorf`,因為它會破壞語意錯誤資訊。

許多撰寫 API 的人並不在乎他們的 API 為不同的輸入傳回哪種類型的錯誤。如果您的 API 是這樣,那麼使用 `fmt.Errorf` 建立錯誤訊息就足夠了,然後在單元測試中,僅測試在您預期錯誤時錯誤是否為非 nil。


此內容是 Go Wiki 的一部分。