Table-Driven Tests
Jest it.each vs Go table-driven tests
หัวข้อที่มีชื่อว่า “Jest it.each vs Go 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:
testingpackage ต้องรันใน binary context ของgo testบล็อกโค้ดด้านล่างคือไฟล์ test จริง — รันบนเครื่องคุณด้วยgo test ./...
เปรียบเทียบ: ทดสอบฟังก์ชันเดิม
หัวข้อที่มีชื่อว่า “เปรียบเทียบ: ทดสอบฟังก์ชันเดิม”// Jest — it.eachfunction 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 — table-driven with t.Runfunc 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) } }) }}กายวิภาคของ table-driven test
หัวข้อที่มีชื่อว่า “กายวิภาคของ table-driven test”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)
รัน specific subtests
หัวข้อที่มีชื่อว่า “รัน specific subtests”# Run all testsgo 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/FAILgo test -v ./...เปรียบเทียบโครงสร้างกับ Jest
หัวข้อที่มีชื่อว่า “เปรียบเทียบโครงสร้างกับ Jest”// Jestdescribe('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(); });});// Gofunc 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(...) } }) }}ทดสอบโค้ดที่อิงเวลา: testing/synctest vs Jest fake timers (Go 1.25+)
หัวข้อที่มีชื่อว่า “ทดสอบโค้ดที่อิงเวลา: testing/synctest vs Jest fake timers (Go 1.25+)”useFakeTimers() ของ Jest ทำให้ test ที่ต้อง sleep หนึ่งชั่วโมงจบได้ทันที Go 1.25 ใส่ไอเดียเดียวกันเข้า standard library: synctest.Test รัน function ของคุณใน “bubble” ที่มี fake clock และ synctest.Wait block จนกว่า goroutine ทุกตัวใน bubble จะว่าง
// Jestjest.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 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 เดียวกัน