การเขียน Tests ด้วย go test
ไฟล์ test และ naming conventions
หัวข้อที่มีชื่อว่า “ไฟล์ test และ naming conventions”test runner ของ Go ค้นหา tests ผ่านกฎ 2 ข้อที่บังคับใช้โดย toolchain:
- ชื่อไฟล์ ลงท้ายด้วย
_test.go— compiler จะไม่รวมไฟล์เหล่านี้ใน build ปกติ - ชื่อฟังก์ชัน ขึ้นต้นด้วย
Testตามด้วยตัวพิมพ์ใหญ่ —TestAdd,TestParseURLเป็นต้น
ไฟล์ test ขั้นต่ำสำหรับ source file math.go อยู่ใน package เดียวกัน และ directory เดียวกัน:
package math
import "testing"
func TestAdd(t *testing.T) { got := Add(2, 3) want := 5 if got != want { t.Errorf("Add(2, 3) = %d; want %d", got, want) }}รันด้วย:
go test ./... # รัน tests ทั้งหมดใน packages ทั้งหมดgo test -v ./... # verbose: แสดงชื่อ test แต่ละตัวและ PASS/FAILgo test -run TestAdd # รันเฉพาะ tests ที่ชื่อตรงกับ regext.Errorf vs t.Fatalf
หัวข้อที่มีชื่อว่า “t.Errorf vs t.Fatalf”ทั้งคู่บันทึกความล้มเหลว แต่ต่างกันตรงที่ test จะหยุดหรือดำเนินต่อ:
| ฟังก์ชัน | บันทึกความล้มเหลว | หยุด test? |
|---|---|---|
t.Errorf | ใช่ | ไม่ — test ดำเนินต่อ |
t.Fatalf | ใช่ | ใช่ — test หยุดทันที |
ใช้ t.Fatalf เมื่อ assertion ถัดไปอาจ panic หาก assertion ก่อนหน้าล้มเหลว (เช่น การ dereference pointer ที่เพิ่งตรวจพบว่าเป็น nil)
func TestDivide(t *testing.T) { result, err := Divide(10, 0) if err == nil { t.Fatalf("expected an error for division by zero, got nil") } _ = result}Table-driven tests ด้วย t.Run
หัวข้อที่มีชื่อว่า “Table-driven tests ด้วย t.Run”รูปแบบ idiomatic ของ Go สำหรับการทดสอบ input หลายค่ากับฟังก์ชันเดียวกันคือ table-driven test: slice ของ struct cases ที่วนซ้ำด้วย t.Run แต่ละ sub-test มีชื่อ, error message, และ — เมื่อใช้ -v — บรรทัดผลลัพธ์เป็นของตัวเอง
package main
import "testing"
func TestFibonacci(t *testing.T) { cases := []struct { name string n int want int }{ {"fib(0)", 0, 0}, {"fib(1)", 1, 1}, {"fib(5)", 5, 5}, {"fib(10)", 10, 55}, }
for _, tc := range cases { tc := tc // capture range variable (pre-Go 1.22) t.Run(tc.name, func(t *testing.T) { got := Fibonacci(tc.n) if got != tc.want { t.Errorf("Fibonacci(%d) = %d; want %d", tc.n, got, tc.want) } }) }}รันด้วย -v เพื่อดู sub-test แต่ละตัว:
go test -v -run TestFibonacci ./...# --- PASS: TestFibonacci (0.00s)# --- PASS: TestFibonacci/fib(0) (0.00s)# --- PASS: TestFibonacci/fib(1) (0.00s)# --- PASS: TestFibonacci/fib(5) (0.00s)# --- PASS: TestFibonacci/fib(10) (0.00s)Race detector
หัวข้อที่มีชื่อว่า “Race detector”เพิ่ม -race เพื่อตรวจจับ data races ในเวลา test — นี่คือสิ่งที่ต้องมีใน CI:
go test -race -coverprofile=coverage.out -covermode=atomic ./...flag -covermode=atomic จำเป็นต้องใช้ร่วมกับ -race เพื่อให้ได้ coverage counts ที่แม่นยำ เนื่องจาก race detector เขียนทับ memory accesses
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| table-driven test | test หลาย case ใน 1 function | ถ้า table ยาวเกิน อ่านยาก |
t.Run() subtests | isolate failure, parallel-friendly | nested output อ่านยากขึ้น |
t.Parallel() | รัน test ขนาน เร็วขึ้น | ต้องระวัง shared state — race condition ง่าย |
-race flag | ตรวจหา race condition อัตโนมัติ | ช้าลง 2-10x — ใช้ใน CI ไม่ใช่ทุก local run |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”t.Fatal()กับt.Error()เหมือนกัน —t.Fatal()หยุด test ทันที,t.Error()mark failed แต่รัน assertion ต่อไปได้go test -cover 100%= code ถูกต้อง — coverage วัดว่า line ถูกรันหรือไม่ ไม่ได้วัดว่า behavior ถูกต้อง — test ไม่มี assertion ก็ทำให้ 100% ได้- ต้องเขียน test file แยก package — Go รองรับทั้ง same package (white-box) และ
_testpackage (black-box) — เลือกตาม use case t.Helper()optional —t.Helper()ทำให้ error report ชี้ไปยัง caller ไม่ใช่ helper function — ควรเรียกเสมอใน helper
💡 ตัวอย่างจากของจริง
Kubernetes controller package ใช้ table-driven test — test ร้อยกรณีใน 1 function, เพิ่ม edge case ได้โดยแค่เพิ่มแถวในตาราง
Go standard library ใช้ table-driven test ทั้งหมด — เป็น pattern ที่ทุก Go codebase ควรทำตาม
Docker enforce
-raceflag ใน CI pipeline ทุก PR เพื่อตรวจสอบ concurrent code