Idiomatic Go Testing: Table Driven Test

Go 測試道地寫法:Table Driven Test

前言

最近在寫 Go 測試時發現官方與社群中有一種大力推崇的慣例:TableDrivenTest🔗,我一直以為測試應該要保持簡單愚蠢,但這種做法讓我想到程式人的三大美德之一:「懶惰」。

什麼是 Table Driven Testing

基本就是把測試會用到的狀態儲存到 Slice 當中,並用迴圈統一執行測試:

func Add(a, b int) int {
return a + b
}
func Test_Add(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"正數相加", 1, 1, 2},
{"加零", 1, 0, 1},
{"負數相加", -1, -1, -2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.expected {
t.Errorf("Add(%d, %d) = %d,預期 %d", tt.a, tt.b, got, tt.expected)
}
})
}
}

與使用之前比對

普通測試寫法每個案例都要複製相同的測試樣板:

func Test_Add(t *testing.T) {
t.Run("正數相加", func(t *testing.T) {
got := Add(1, 1)
if got != 2 {
t.Errorf("Add(1, 1) = %d,預期 2", got)
}
})
t.Run("加零", func(t *testing.T) {
got := Add(1, 0)
if got != 1 {
t.Errorf("Add(1, 0) = %d,預期 1", got)
}
})
t.Run("負數相加", func(t *testing.T) {
got := Add(-1, -1)
if got != -2 {
t.Errorf("Add(-1, -1) = %d,預期 -2", got)
}
})
}

每新增一個測試案例就得把測試邏輯再抄一次,而 Table Driven Test 把「測試邏輯」與「測試資料」分離,編輯案例只需要在 slice 裡多加一行 struct,測試邏輯本身完全不用動。

總結

我好奇為什麼其他語言不太出現這種測試模式,可能是因為這種模式通常只是測試框架包裝下的方便手段,而 Go 社群文化下人人對於追求簡單程式有一種莫名的執著。

test.each([
["正數相加", 1, 1, 2],
["加零", 1, 0, 1],
["負數相加", -1, -1, -2],
])("%s", (name, a, b, expected) => {
expect(add(a, b)).toBe(expected);
});

延伸閱讀