ข้ามไปยังเนื้อหา

Table-Driven Tests

ใน Jest คุณหยิบ it.each หรือ describe.each มาใช้เมื่อต้องรัน assertion logic เดิมซ้ำกับ input หลาย ๆ ชุด โดยไม่ต้อง copy-paste ตัว test function ส่วน Go ไม่มี it.each มาให้ในตัว แต่ community ลงตัวกันที่ idiom เดียว คือ slice ของ anonymous struct (“table”) ที่วน loop อยู่ใน test function เดียว แล้วรันแต่ละ case เป็น subtest ผ่าน t.Run

หมายเหตุเรื่อง Playground: testing package ต้องรันใน binary context ของ go test บล็อกโค้ดด้านล่างคือไฟล์ test จริง — รันบนเครื่องคุณด้วย go test ./...

TypeScript
// Jest — it.each
function add(a: number, b: number) { return a + b; }
it.each([
[1, 2, 3],
[0, 0, 0],
[-1, 1, 0],
[10, 20, 30],
])('add(%i, %i) = %i', (a, b, want) => {
expect(add(a, b)).toBe(want);
});
Go
// Go — table-driven ด้วย t.Run
func add(a, b int) int { return a + b }
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive", 1, 2, 3},
{"zeros", 0, 0, 0},
{"negative", -1, 1, 0},
{"large", 10, 20, 30},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := add(tc.a, tc.b)
if got != tc.want {
t.Errorf("add(%d, %d) = %d; want %d", tc.a, tc.b, got, tc.want)
}
})
}
}
math_test.go
package math_test
import "testing"
func TestDivide(t *testing.T) {
// 1. Table — slice ของ anonymous structs
tests := []struct {
name string
a, b float64
want float64
wantErr bool
}{
{"normal", 10, 2, 5, false},
{"fraction", 7, 2, 3.5, false},
{"divide_by_zero", 1, 0, 0, true},
}
// 2. วนลูปใน table
for _, tc := range tests {
// 3. t.Run สร้าง named subtest
t.Run(tc.name, func(t *testing.T) {
// 4. t.Parallel() — optional, รัน subtests พร้อมกัน
t.Parallel()
got, err := Divide(tc.a, tc.b)
if (err != nil) != tc.wantErr {
t.Fatalf("Divide() error = %v, wantErr %v", err, tc.wantErr)
}
if !tc.wantErr && got != tc.want {
t.Errorf("Divide() = %v; want %v", got, tc.want)
}
})
}
}

convention สำคัญ:

  • t.Errorf ทำเครื่องหมายว่า fail แต่ test ยังรันต่อ
  • t.Fatalf ทำเครื่องหมายว่า fail แล้วหยุด subtest ปัจจุบันทันที
  • t.Parallel() ภายใน t.Run ให้ subtest รันพร้อมกันได้ (ปลอดภัยเมื่อแต่ละ case เป็นอิสระต่อกัน)
  • field name ของแต่ละ case จะกลายเป็น label ของ subtest (TestDivide/normal, TestDivide/divide_by_zero)
Terminal window
# รัน tests ทั้งหมด
go test ./...
# รันเฉพาะ subtest ที่มีชื่อ (รองรับ regex)
go test -run TestDivide/divide_by_zero ./...
# รัน subtests ทั้งหมดที่ชื่อมี "zero"
go test -run TestDivide/.*zero.* ./...
# Verbose output — แสดงชื่อ subtest แต่ละตัวและ PASS/FAIL
go test -v ./...
TypeScript
// Jest
describe('Divide', () => {
it.each([
['normal', 10, 2, 5],
['fraction', 7, 2, 3.5],
])('%s', (_, a, b, want) => {
expect(divide(a, b)).toBe(want);
});
it('throws on divide by zero', () => {
expect(() => divide(1, 0)).toThrow();
});
});
Go
// Go
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b float64
want float64
wantErr bool
}{
{"normal", 10, 2, 5, false},
{"fraction", 7, 2, 3.5, false},
{"by_zero", 1, 0, 0, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := Divide(tc.a, tc.b)
if (err != nil) != tc.wantErr { t.Fatal(...) }
if !tc.wantErr && got != tc.want { t.Errorf(...) }
})
}
}
ใน Go table-driven test อะไรที่สร้าง named subtest สำหรับแต่ละ case?
ความแตกต่างระหว่าง t.Errorf และ t.Fatalf คืออะไร?
วิธีรัน subtest ชื่อ "divide_by_zero" ใน TestDivide?
Table ใน Go table-driven test มักถูกกำหนดเป็น: