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ì

Ảnh chụp bảng nền tối khung chọn loại test theo câu hỏi cần trả lời với 11 phần 11 bộ số đo thật mỗi dòng câu hỏi loại test bằng chứng P là phần, về logic hàm thuần luật nghiệp vụ gồm đúng với ca tôi nghĩ ra dùng unit table-driven 6 micro giây mỗi ca 2 dòng mỗi ca lõi thuần 100 phần trăm nhánh P2, đúng với ca tôi không nghĩ ra dùng property-based ví dụ xanh 4 trên 4 tính chất đỏ 4 trên 4 ca đầu P5, test khẳng định gì hay chỉ chạy dùng mutation testing cùng 100 phần trăm phủ giết 2 trên 10 so với 8 trên 10 P7, đầu vào ngoài có làm panic dùng fuzzing 8 trên 8 crash dưới 1,6 giây thu nhỏ còn 1 tới 4 byte P6, về ghép nối DB HTTP dịch vụ khác gồm SQL này chạy trên production dùng PostgreSQL thật SQLite lọt 6 trên 6 PG bắt 6 trên 6 rollback 2,9 ms P4, hàm đúng nhưng có được gọi dùng integration hoặc e2e unit 2 trên 8 integration 4 trên 8 e2e 8 trên 8 P1, tác dụng phụ có xảy ra dùng fake không mock fake bắt 4 trên 4 vỡ 0 trên 4 mock bắt 4 trên 4 vỡ 4 trên 4 P3, provider đổi API ai gãy dùng contract CDC bắt 4 trên 4 nêu đúng consumer 0 báo oan P8, về thời gian và sự cố gồm có đua dữ liệu dùng race 20 trên 20 mù với đua logic 0 báo cáo P10, có đua logic hoặc deadlock dùng ép lịch Gosched 0 trên 1000 chạy thường sang 100 trên 100 có điểm nhường P10, giòn vì máy hay vì code dùng count shuffle cpu đua 91 0 100 phần trăm theo cách chạy sửa 0 trên 100 P9, timeout retry breaker có chạy dùng fault injection timeout 1 giây thua latency 300 ms breaker 22 sang 7 P11, ba quy tắc đứng sau bảng gồm 1 giá nhảy ở chỗ chạm I O 12,5 micro giây sang 2,3 ms gấp 184 luật trong lõi thuần I O ở vỏ số ca tỉ lệ nghịch với giá, 2 khẳng định hệ quả không khẳng định cách làm fake thay mock tính chất thay ví dụ trạng thái thay lời gọi, 3 thứ có xác suất phải thành chắc chắn trước khi vào CI race shuffle goleak điểm nhường toxic 100 phần trăm

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

Ảnh chụp output thật nền tối cả sê-ri chạy lại hết 1,1 giây thường 5,6 giây race với go-lab 10 CPU 9 mô-đun demo P1 tới P10 P3 chỉ có biến thể sinh P11 cần Toxiproxy đã gỡ lỗi cấy sẵn giữ nguyên theo mặc định, mỗi mô-đun go test count 1 lần chạy nóng build đã cache gồm P1 shop 4 test 0,12 giây thường 1,19 giây race unit tích hợp PG handler, P2 t2 32 test 0,19 và 1,32 giây 24 ca bảng và vỏ, P4 t4 7 test 0,25 và 0,43 giây PG thật 1 đỏ là TestDialect SQLite lệch cố ý, P5 t5 9 test 0,08 và 0,13 giây 5 đỏ là 4 lỗi cấy và 1 ví dụ đóng băng PBT_FIX chưa bật, P6 t6 2 test 0,07 và 0,10 giây 2 đỏ là corpus crash trong testdata fuzz FUZZ_FIX chưa bật, P7 t7 2 test 0,06 và 1,10 giây, P8 t8 9 test 0,17 và 1,24 giây provider consumer hợp đồng, P9 t9 7 test 0,09 và 0,10 giây 1 tới 2 đỏ tuỳ lượt test giòn giữ nguyên để đo, P10 t10 3 test 0,06 giây và race timeout xem dưới, tổng trừ P10 75 test 1,03 giây thường 5,61 giây race gấp 5,4 vẫn dưới 6 giây cho cả sê-ri, lần chạy lạnh đầu tiên với race biên dịch có đo đạc chiếm gần hết gồm P1 shop 4,18 giây sang 1,19 giây P4 t4 7,83 giây sang 0,43 giây pgx và modernc sqlite tổng lạnh 17,0 giây sang nóng 5,6 giây nên race không chậm gấp 10 như hay nói thứ chậm là biên dịch lại phụ thuộc có đo đạc và cache lo được, P10 dưới race deadlock từng 0 trên 20 kẹt ở chạy thường giờ kẹt 2 trên 2 tới hết timeout gồm go test race timeout 10s FAIL TestCounterRace đua dữ liệu đúng kỳ vọng panic test timed out after 10s là TestTransferBothWays kẹt, go test race count 5 timeout 60s run TestTransferBothWays panic test timed out after 1m0s, race đổi lịch chạy đủ để deadlock thứ tự khoá lộ ra P10 đo 0 trên 20 khi chạy thường không timeout thì job treo 10 phút mặc định, checklist CI rút từ số đo gồm go test race shuffle on timeout 120s mỗi commit 5,6 giây cho 75 test deadlock và thứ tự lộ ngay, tích hợp PG thật rollback mỗi test 2,9 ms mỗi commit SQLite thay thế lọt 6 trên 6 lỗi P4, corpus fuzz trong testdata fuzz chạy cùng unit mỗi commit 0,07 giây fuzz tìm mới theo lịch đêm P6, mutation testing gói lõi thuần theo lịch gremlins timeout coefficient đọc từng con sống P7, hợp đồng consumer-driven chạy ở CI provider mỗi PR của provider 0 báo oan P8, fault injection tuần tự gỡ trong t.Cleanup theo lịch khẳng định tất định bản xác suất đỏ 17 trên 20 P11, không retry test đỏ trong CI vì che xác suất thay vì loại nguồn P9, không mock đóng đinh chuỗi lời gọi vì vỡ 4 trên 4 refactor giữ nguyên hành vi P3

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; -shuffle phơi phụ thuộc thứ tự (54% ở phần 9), -timeout bắ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/fuzz chạ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 -fuzz và -fuzztime và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=50 cho 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 -race xanh 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ề

  1. 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.
  2. Bộ test đúng cấu trúc rẻ đến bất ngờ: cả sê-ri 1,03 giây, -race 5,61 giây; cái đắt là biên dịch lạnh (17 giây) và cache lo được. Bật -race -shuffle -timeout mỗi commit không cần cân nhắc.
  3. Thứ có xác suất phải thành chắc chắn trước khi vào CI — và -race vừ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