บทที่ 20 · Part 6 — Evidence-Driven Quality
Testing Concurrent Code
ทดสอบ cancellation, ordering และ time-based behavior แบบ deterministic ด้วย synctest และ race detector
test concurrency ที่ใช้ time.Sleep(100 * time.Millisecond) มักมี 2 โหมด: ช้ากว่าที่จำเป็นเมื่อเครื่องว่าง
และ flaky เมื่อ CI ช้า การเพิ่ม sleep เป็น 1 วินาทีเพียงเลื่อนปัญหา เราต้อง synchronize กับ event ที่
สนใจและควบคุมเวลา แทนการเดาว่า goroutine “น่าจะทำเสร็จแล้ว”
จบบทนี้คุณจะ
- ทดสอบ ordering, cancellation, backpressure และ shutdown โดยไม่พึ่ง wall-clock delay
- ใช้
testing/synctest, race detector และgoleakคนละหน้าที่ - ออกแบบ seam ที่ทำให้ concurrency observable โดยไม่ expose implementation เกินไป
Test Protocol ไม่ใช่ Scheduler
สิ่งที่ test ควร assert คือ protocol เช่น worker ไม่เริ่มเกิน limit, cancellation ทำให้ Wait คืน,
channel ปิดหลังผลลัพธ์สุดท้าย หรือ blocked producer ถูกปล่อยเมื่อ consumer รับงาน อย่า assert ว่า
goroutine A จะรันก่อน B ถ้า contract ไม่ได้กำหนด
ใช้ channel ใน fake dependency เป็น rendezvous point:
started := make(chan struct{})
release := make(chan struct{})
process := func(ctx context.Context, job Job) error {
close(started)
select {
case <-release:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
worker.Start(t.Context(), process)
<-started
close(release)
test รู้ว่า job เริ่มเพราะรับ signal ไม่ใช่เพราะผ่านไป 20 ms Channel นี้เป็น test control point ที่สะท้อน protocol จริง แต่ไม่ควรเพิ่ม hook เข้า production API ถ้า fake dependency ให้ observation ได้แล้ว
testing/synctest ควบคุมเวลาและ Blocking
Go 1.25 ทำ testing/synctest เป็น package ปกติ synctest.Test สร้าง bubble ที่เวลาจำลองเดินหน้า
เมื่อ goroutines ใน bubble durable-blocked เหมาะกับ timer, context deadline และ retry backoff:
func TestRetryStopsAtDeadline(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
done := make(chan error, 1)
go func() {
done <- retry(ctx, failingOperation)
}()
time.Sleep(5 * time.Second)
synctest.Wait()
if err := <-done; !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("retry() error = %v, want deadline exceeded", err)
}
})
}
เวลา 5 วินาทีผ่านแบบรวดเร็วและ deterministic synctest.Wait รอให้ goroutines ถึงจุด blocked/stable
ก่อน assert แต่ package นี้ไม่ใช่คำตอบสำหรับ external network/process หรือทุก custom synchronization
primitive อ่าน package contract และแยก integration test เมื่อ dependency อยู่นอก bubble
ถ้า module ยังต่ำกว่า Go 1.25 ใช้ fake clock ที่ inject ผ่าน interface หรือ clock library แทนการนำ
experimental API เก่ามาใช้ สำหรับคอร์สนี้ baseline 1.25 จึงใช้ public synctest.Test
เครื่องมือ 3 ตัวตอบคนละคำถาม
race detector ต้อง execute path จึงจะเห็น race; เพิ่ม stress/repeat บน package สำคัญได้ แต่ไม่ใช้
ความถี่แทน synchronization go.uber.org/goleak เหมาะกับ package ที่สร้าง long-lived goroutines
และควร allowlist เฉพาะ goroutine ที่รู้จักจริง ไม่ ignore กว้างจนซ่อน leak ใหม่
go test -race ./...
go test -race -count=50 ./internal/worker
test timeout ระดับ go test -timeout เป็น safety net ให้ stack dump เมื่อ deadlock ไม่ใช่ synchronization
mechanism ของ assertion
Scenario ที่ Worker ต้องผ่าน
สำหรับ bounded worker จากบทก่อน ให้มี tests อย่างน้อย:
- ทำครบทุก job และคืน error ที่ contract กำหนด
- active jobs ไม่เกิน concurrency limit โดยวัด max in-flight แบบ synchronized
- queue เต็มแล้ว block/reject/drop ตาม policy
- cancel ก่อนเริ่ม, ระหว่าง I/O และระหว่างส่งผลลัพธ์แล้วหยุดได้
- error แรก cancel sibling ถ้าใช้ errgroup
- close/shutdown เรียกซ้ำตาม contract และไม่มี send-on-closed-channel
- ไม่มี goroutine leak เมื่อ dependency fail หรือ caller เลิกรับผลลัพธ์
Production Toolbox
Default: channel rendezvous สำหรับ event, testing/synctest สำหรับเวลา/context, -race สำหรับ
memory access และ go.uber.org/goleak สำหรับ lifetime ใช้ fake clock เมื่อ code ต้องรองรับ Go เก่า
หรือ dependency อยู่นอก synctest model ไม่ใช้ sleep เป็น assertion
Checklist ของ Concurrent Test
- test synchronize กับ event ที่ต้องการ ไม่เดาเวลา
- ordering ถูก assert เฉพาะเมื่อเป็น public contract
- success/error/cancel/full/shutdown paths ถูกเรียกจริง
- race detector และ leak detector รันแยกจาก behavior assertion
- fake/control channel มี buffer/owner ที่ไม่ทำให้ test เป็นฝ่าย deadlock
- test timeout ให้ diagnostic แต่ไม่ได้ทำให้ behavior ผ่าน
- flake ถูกหาต้นเหตุ ไม่แก้ด้วย retry test หรือเพิ่ม sleep
อ่านเพิ่ม: testing/synctest,
Data Race Detector,
Testing Time และ
go.uber.org/goleak