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

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 with 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. The table — a slice of 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. Loop over the table
for _, tc := range tests {
// 3. t.Run creates a named subtest
t.Run(tc.name, func(t *testing.T) {
// 4. t.Parallel() — optional, runs subtests concurrently
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
# Run all tests
go test ./...
# Run only one subtest by name (supports regex)
go test -run TestDivide/divide_by_zero ./...
# Run all subtests whose name contains "zero"
go test -run TestDivide/.*zero.* ./...
# Verbose output — prints each subtest name and 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(...) }
})
}
}

useFakeTimers() ของ Jest ทำให้ test ที่ต้อง sleep หนึ่งชั่วโมงจบได้ทันที Go 1.25 ใส่ไอเดียเดียวกันเข้า standard library: synctest.Test รัน function ของคุณใน “bubble” ที่มี fake clock และ synctest.Wait block จนกว่า goroutine ทุกตัวใน bubble จะว่าง

TypeScript
// Jest
jest.useFakeTimers();
test('debounce fires once', () => {
const fn = jest.fn();
const d = debounce(fn, 1000);
d(); d(); d();
jest.advanceTimersByTime(1000);
expect(fn).toHaveBeenCalledTimes(1);
});
Go
// Go 1.25+
import "testing/synctest"
func TestDebounce(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
calls := 0
d := debounce(func() { calls++ }, time.Second)
d(); d(); d()
time.Sleep(time.Second) // fake clock: returns instantly
synctest.Wait() // let the timer goroutine run
if calls != 1 {
t.Fatalf("calls = %d, want 1", calls)
}
})
}
// Go 1.27+: synctest.Sleep(d) == time.Sleep(d) + synctest.Wait()

ใน bubble ทั้ง time.Now, time.Sleep, timer และ ticker ใช้ virtual clock ทั้งหมด test จึงเร็วและ deterministic ใช้คู่กับ httptest.NewTestServer() (Go 1.27) จะได้ HTTP server ในหน่วยความจำที่อยู่ใน bubble เดียวกัน

ใน Go table-driven test อะไรที่สร้าง named subtest สำหรับแต่ละ case?
ความแตกต่างระหว่าง t.Errorf และ t.Fatalf คืออะไร?
วิธีรัน subtest ชื่อ "divide_by_zero" ใน TestDivide?
Table ใน Go table-driven test มักถูกกำหนดเป็น: