Mười một phần trước mỗi phần đo một kỹ thuật: cấy lỗi, chạy, đếm. Phần này không thêm kỹ thuật mới mà gom những con số đó lại để trả lời câu hỏi thực dụng nhất: đứng trước một đoạn mã, chọn loại test nào, và CI nên chạy gì khi nào. Để kết luận vẫn có số đo đứng sau, toàn bộ mô-đun demo của sê-ri được chạy lại một lượt — thường và với -race — để biết cả sê-ri tốn bao nhiêu giây, và có bất ngờ gì không. Có một.
Khung chọn: hỏi câu gì, dùng test gì

Hình 1: Mười hai câu hỏi, mười hai loại test, mỗi dòng có một số đo trong sê-ri đứng sau. Ba quy tắc ở cuối là thứ sinh ra cả bảng.
Bảng trên sắp xếp theo câu hỏi, không theo tên kỹ thuật, vì đó là thứ tự người viết test gặp trong thực tế: có một đoạn mã, lo một chuyện cụ thể, cần biết test nào trả lời chuyện đó. Ba nhóm câu hỏi:
Về logic — hàm thuần, luật nghiệp vụ. Unit table-driven cho ca bạn nghĩ ra (phần 2: 6 µs và 2 dòng mỗi ca, sau khi tách I/O ra vỏ). Property-based cho ca bạn không nghĩ ra (phần 5: ví dụ xanh 4/4, tính chất đỏ 4/4 ngay ca đầu). Mutation testing để biết test có khẳng định gì không (phần 7: cùng 100% phủ, giết 2/10 với 8/10). Fuzzing cho đầu vào từ ngoài (phần 6: 8/8 crash dưới 1,6 giây).
Về ghép nối — cơ sở dữ liệu, HTTP, dịch vụ khác. PostgreSQL thật thay SQLite (phần 4: SQLite lọt 6/6 lỗi, rollback mỗi test chỉ 2,9 ms). Integration và end-to-end cho câu "hàm đúng nhưng có được gọi không" (phần 1: 2/8, 4/8, 8/8 theo tầng). Fake thay mock cho tác dụng phụ (phần 3: fake bắt 4/4 lỗi và vỡ 0/4 refactor). Hợp đồng consumer-driven cho ranh giới dịch vụ (phần 8: 4/4, đúng consumer, 0 báo oan).
Về thời gian và sự cố — thứ không lộ ở lượt chạy đầu. -race cho đua dữ liệu và chỉ đua dữ liệu (phần 10: 20/20, 0 báo cáo cho đua logic). Ép lịch chạy cho đua logic và deadlock (0/1.000 thành 100/100 với một Gosched). -count, -shuffle, -cpu 1 để biết test giòn vì máy hay vì mã (phần 9). Fault injection cho timeout, retry, breaker (phần 11: timeout 1 giây thua latency 300 ms).
Ba quy tắc sinh ra bảng: giá nhảy ở chỗ chạm I/O (×184 ở phần 1) nên số ca phải tỉ lệ nghịch với giá; khẳng định hệ quả, không khẳng định cách làm (fake, tính chất, trạng thái); và cái gì có xác suất phải thành chắc chắn trước khi vào CI.
Chạy lại cả sê-ri: giá thật của bộ test

Hình 2: Chín mô-đun demo chạy lại. Lỗi cấy sẵn giữ nguyên nên các dòng đỏ là cố ý. Bất ngờ nằm ở dòng P10.
| thường | -race nóng |
-race lạnh |
|
|---|---|---|---|
| 75 test, 8 mô-đun (trừ P10) | 1,03 s | 5,61 s | 17,0 s |
| P10 (3 test đồng thời) | 0,06 s | timeout 10 s | timeout 120 s |
Cả sê-ri rẻ hơn một lần mở trình duyệt. 75 test của tám mô-đun — có cả tích hợp PostgreSQL thật, provider HTTP, hợp đồng — chạy xong trong 1,03 giây; với -race là 5,61 giây, gấp 5,4 lần, vẫn dưới sáu giây. Những phần đắt nhất là phần chạm cơ sở dữ liệu (P4: 0,25 giây cho 7 test) và phần có nhiều goroutine dưới -race (P1, P2, P8 mỗi cái hơn một giây). Không có lý do chi phí nào để -race không bật mặc định.
-race không "chậm gấp 10", thứ chậm là biên dịch lạnh. Lần đầu chạy -race, tổng là 17,0 giây; P4 mất 7,83 giây rồi lần sau 0,43 — vì pgx và modernc sqlite phải biên dịch lại với đo đạc. Build cache lo chuyện này; trên CI, cache thư mục build của Go là bước đáng giá nhất để -race thành miễn phí.
Bất ngờ: deadlock của phần 10 kẹt 2/2 dưới -race. Phần 10 đo TestTransferBothWays hoàn thành 20/20 lượt chạy thường và chỉ kẹt khi chèn điểm nhường. Chạy lại với -race: kẹt tới hết timeout cả hai lần, 10 giây và 60 giây. -race làm chương trình chậm và đổi lịch chạy đủ để hai goroutine cùng lọt vào cửa sổ giữa hai lần khoá — đúng cơ chế đã làm đua logic lộ 15–26% ở phần 10. Hệ quả cho CI: -race có thể phơi deadlock mà chạy thường không thấy, và vì runtime Go không báo deadlock trong go test, không có -timeout thì job treo 10 phút mặc định rồi bị giết mà không có goroutine dump hữu ích.
Các dòng đỏ còn lại là cố ý. P5 đỏ 5 (bốn lỗi cấy và một ví dụ đóng băng hành vi sai, PBT_FIX chưa bật), P6 đỏ 2 (corpus crash trong testdata/fuzz, FUZZ_FIX chưa bật), P4 đỏ 1 (TestDialect ghi nhận SQLite lệch PostgreSQL), P9 đỏ 1–2 tuỳ lượt (test giòn giữ nguyên để đo). Chúng cho thấy một chuyện phụ: test đỏ "cố ý" trong một kho thật là nợ — hai tuần sau không ai nhớ dòng nào cố ý. Ở kho thật, lỗi cấy phải nằm sau cờ tắt mặc định, hoặc nằm trong test khẳng định lỗi được bắt, không phải test đỏ.
Checklist CI
Rút từ số đo, chia theo nhịp:
Mỗi commit (ngân sách: vài giây đến vài phút)
go test -race -shuffle=on -timeout 120s ./...— 5,6 giây cho 75 test;-shufflephơi phụ thuộc thứ tự (54% ở phần 9),-timeoutbắt buộc vì deadlock không tự báo.- Tích hợp với PostgreSQL thật, rollback mỗi test (2,9 ms) hoặc database mỗi test (20,5 ms) khi mã tự quản lý transaction; container mỗi gói (0,8 giây), không mỗi test.
- Corpus fuzz trong
testdata/fuzzchạy như unit (0,07 giây) — mỗi file crash đã tìm được là một hồi quy. - Hợp đồng consumer-driven chạy ở CI của provider, với mọi PR đổi API.
Theo lịch (đêm, hoặc trước release)
- Fuzz tìm mới với
-fuzzvà-fuzztimevài phút trên hàm đọc đầu vào ngoài. - Mutation testing trên gói lõi thuần, nhớ
--timeout-coefficient; đọc từng đột biến sống thay vì nhìn điểm. - Fault injection với Toxiproxy: tuần tự, gỡ toxic trong
t.Cleanup, khẳng định tất định (toxic 100%, đếm lần thử) — bản xác suất đỏ 17/20. -count=50cho gói có goroutine, để biết tỉ lệ giòn trước khi nó thành thói quen bấm chạy lại.
Không làm
- Retry test đỏ trong CI: che xác suất thay vì loại nguồn; phần 9 sửa được 0/100 ở mọi cách chạy cho cả năm nguồn.
- Mock đóng đinh chuỗi lời gọi: vỡ 4/4 refactor giữ nguyên hành vi.
- SQLite trong bộ nhớ thay PostgreSQL để "cho nhanh": lọt 6/6 lỗi, và rollback với PostgreSQL thật đã rẻ hơn cả không dọn.
- Coi
-racexanh là "đồng thời đã an toàn": 0 báo cáo cho đua logic.
Phạm vi của cả sê-ri
Mọi con số đo trên một máy 10 CPU, Go 1.23, PostgreSQL 16 cùng mạng Docker; mô-đun demo nhỏ (vài trăm dòng) nên giá tuyệt đối thấp hơn kho thật nhiều lần, còn tỉ lệ — ×184 khi chạm cơ sở dữ liệu, ×5,4 với -race, 91%/0%/100% theo cách chạy — là thứ mang sang được. Những gì sê-ri không chạm: test giao diện, test hiệu năng có ngưỡng, kiểm thử trên môi trường staging, và các bộ công cụ ngoài Go. Khung chọn theo câu hỏi không phụ thuộc ngôn ngữ; con số thì có.
Ba ý mang về
- Chọn test theo câu hỏi, không theo tên kỹ thuật: mười hai câu hỏi trong bảng phủ gần hết lo lắng thực tế, và mỗi câu có một loại test trả lời rẻ nhất — với số đo đứng sau.
- Bộ test đúng cấu trúc rẻ đến bất ngờ: cả sê-ri 1,03 giây,
-race5,61 giây; cái đắt là biên dịch lạnh (17 giây) và cache lo được. Bật-race -shuffle -timeoutmỗi commit không cần cân nhắc. - Thứ có xác suất phải thành chắc chắn trước khi vào CI — và
-racevừa chứng minh điều ngược lại cũng đúng: nó phơi deadlock 2/2 mà chạy thường 0/20. Không-timeout, bài học đó đổi thành job treo 10 phút.
Nguồn
- Go — testing package (
-race,-shuffle,-timeout,-count, fuzzing): https://pkg.go.dev/testing - Go — Data Race Detector: https://go.dev/doc/articles/race_detector
- Google Testing Blog — Test Sizes (small/medium/large theo tài nguyên): https://testing.googleblog.com/2010/12/test-sizes.html
- Martin Fowler — Test Pyramid, Mocks Aren't Stubs, ContractTest: https://martinfowler.com/bliki/TestPyramid.html
- Shopify — Toxiproxy: https://github.com/Shopify/toxiproxy
- Mười một phần trước của sê-ri, mỗi phần kèm mã demo và số đo: từ phần 1 đến phần 11.