Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng buổi sáng thứ Hai, bạn mở Slack và thấy tin nhắn từ team lead: "Sao build tối qua đỏ vậy em? Anh chạy lại thì nó xanh." Bạn chạy lại lần thứ ba, xanh thật. Không ai sửa dòng code nào cả. Đó chính là flaky test — bài kiểm thử "hôm pass hôm fail" dù mã nguồn và môi trường không thay đổi.
Nghe qua thì có vẻ vô hại: chạy lại vài lần là xanh mà. Nhưng flaky test là kẻ thù số một của lòng tin vào toàn bộ test suite. Khi một test thỉnh thoảng fail vô cớ, con người sẽ nhanh chóng hình thành phản xạ nguy hiểm: bấm "Retry". Và một khi cả team đã quen retry, thì cái ngày một test thật sự bắt được bug thật, mọi người vẫn sẽ bấm retry — rồi merge code lỗi vào production.
Có một câu nói kinh điển trong giới automation: "Một test suite có flaky test còn tệ hơn không có test suite nào." Vì test suite không có gì thì bạn biết mình không được bảo vệ. Còn test suite flaky khiến bạn tưởng mình được bảo vệ, trong khi thực chất nó đang dạy cả team bỏ qua tín hiệu cảnh báo.
Google từng công bố rằng khoảng 1.5% số lần chạy test của họ cho kết quả flaky, và gần 16% tổng số test của họ có biểu hiện flaky ở một thời điểm nào đó. Với quy mô hàng triệu lần chạy mỗi ngày, đó là con số khổng lồ về thời gian và niềm tin bị bào mòn. Bài học này sẽ dạy bạn cách nhận diện, phân loại, sửa và quản lý flaky test một cách có hệ thống — thay vì chỉ ngồi bấm retry và cầu nguyện.
Khái niệm cốt lõi
Flaky test là gì (và không phải là gì)
Định nghĩa chuẩn: Flaky test là test cho ra kết quả khác nhau (pass/fail) trên cùng một phiên bản code, cùng một cấu hình, mà không có thay đổi nào từ phía bạn.
Cần phân biệt rõ ràng với hai trường hợp KHÔNG phải flaky:
- Test fail vì code thật sự có bug → đó là test làm đúng việc, không flaky.
- Test fail vì bạn vừa đổi môi trường (nâng version thư viện, đổi data) → đó là môi trường thay đổi, không flaky.
Những nguyên nhân phổ biến nhất
Đây là bảng "tra bệnh" mà bạn nên thuộc lòng. Phần lớn flaky test trên đời rơi vào một trong các nhóm sau:
| Nguyên nhân | Biểu hiện | Giải pháp cốt lõi |
|---|---|---|
| Timing / race condition | Test click nút trước khi phần tử render xong; kết quả tùy tốc độ máy | Dùng explicit wait, chờ điều kiện cụ thể thay vì sleep cứng |
| Test order dependency | Test A pass khi chạy một mình, fail khi chạy sau test B | Mỗi test tự setup/teardown dữ liệu riêng, không chia sẻ state |
| Shared / dirty state | DB, biến global, file tạm còn sót dữ liệu từ test trước | Reset state ở beforeEach; dùng transaction rollback |
| Async chưa hoàn tất | Gọi API/animation chưa xong đã assert | Await/poll đúng điều kiện hoàn thành, không đoán thời gian |
| Thời gian & múi giờ | Test fail lúc 00:00, hoặc chỉ fail ở giờ VN (UTC+7) | Fix cứng clock (mock time), luôn set timezone rõ ràng |
| Dữ liệu ngẫu nhiên | Faker sinh chuỗi rỗng/ký tự lạ làm vỡ validation | Seed cố định cho random; kiểm soát biên dữ liệu |
| Phụ thuộc mạng / dịch vụ ngoài | Gọi API bên thứ ba, thỉnh thoảng timeout | Mock/stub dịch vụ ngoài trong test tự động |
| Resource leak / bộ nhớ | Fail khi chạy song song nhiều test, hết cổng/kết nối | Đóng tài nguyên đúng cách; giới hạn concurrency |
Vì sao flaky test nguy hiểm hơn bạn nghĩ
Có ba tầng tác hại, tăng dần:
- Tốn thời gian trực tiếp: mỗi lần retry là vài phút CI, nhân với hàng trăm lần mỗi tuần.
- Bào mòn niềm tin: team bắt đầu bỏ qua test đỏ, "chắc lại flaky ấy mà".
- Che giấu bug thật: đây là tầng chết người. Khi retry đã thành thói quen, test bắt được lỗi thật cũng bị retry cho qua.
Tình huống thực tế
Ví dụ 1 — Tiki và cơn ác mộng "flaky trước giờ deploy" (bối cảnh giả định hợp lý)
Một team QA tại một sàn thương mại điện tử lớn ở TP.HCM (quy mô tương tự Tiki) có suite khoảng 1.200 E2E test chạy trên Selenium Grid. Trong giai đoạn cao điểm sale 12/12, tỷ lệ build đỏ vì flaky lên tới 1 trên 4 lần chạy. Mỗi lần deploy phải chạy lại pipeline trung bình 2–3 lần, kéo thời gian release từ 20 phút lên gần một tiếng.
Khi điều tra, team phát hiện phần lớn flaky đến từ các test liên quan giỏ hàng: test thêm sản phẩm vào giỏ rồi ngay lập tức assert số lượng badge trên header. Trên máy CI mạnh thì kịp, trên runner yếu thì badge chưa kịp cập nhật qua AJAX → fail. Đây là timing/async kinh điển.
Cách xử lý: thay vì Thread.sleep(1000), họ chuyển sang explicit wait chờ đúng điều kiện badge hiển thị đúng số. Đồng thời, họ mock lại một số dịch vụ khuyến mãi bên thứ ba vốn hay timeout mùa sale. Kết quả: tỷ lệ flaky giảm từ 25% xuống dưới 3% trong hai tuần.
Bài học: Flaky thường bùng lên chính xác vào lúc bạn cần suite ổn định nhất — mùa cao điểm. Đừng đợi đến khi cháy nhà mới đi sửa vòi nước.
Ví dụ 2 — Bug thật bị chôn dưới lớp retry (Knight Capital như bài học đối chiếu)
Không phải flaky test, nhưng câu chuyện Knight Capital năm 2012 là minh họa hoàn hảo cho hậu quả của việc "làm ngơ tín hiệu". Họ deploy code chưa được kiểm thử đầy đủ và mất 440 triệu USD trong 45 phút. Điểm liên quan đến chúng ta: khi một tổ chức đã quen bỏ qua các tín hiệu bất thường (dù là alert hay test đỏ), thảm họa chỉ là vấn đề thời gian.
Ở quy mô nhỏ hơn, tôi từng thấy một fintech ở Singapore có test kiểm tra luồng hoàn tiền. Test này flaky suốt hai tháng, cả team quen tay retry. Đến một ngày, một thay đổi thật làm hỏng logic làm tròn số tiền hoàn — và test fail vì lý do thật. Nhưng vì đã có "danh tiếng flaky", dev retry ba lần, thấy vẫn đỏ nhưng nghĩ "hôm nay CI dở", rồi ép merge. Bug lọt lên production, ảnh hưởng khoảng 300 giao dịch trước khi bị phát hiện.
Bài học: Một khi test đã mang danh "flaky" nhưng không được cách ly (quarantine) rõ ràng, nó trở thành điểm mù. Test đỏ mà không ai tin nữa còn nguy hiểm hơn không có test.
Ví dụ 3 — Múi giờ UTC+7 và cái bẫy nửa đêm (bối cảnh VN)
Một team ở Hà Nội viết test kiểm tra tính năng "đơn hàng tạo hôm nay". Test tạo đơn, rồi query những đơn có created_date == today. Chạy ban ngày thì xanh mượt. Nhưng cứ những lần CI chạy khoảng 17h giờ UTC (tức 00h giờ VN) thì fail.
Nguyên nhân: server CI chạy theo UTC, còn logic ứng dụng dùng giờ VN (UTC+7). Ngay khoảnh khắc chuyển ngày ở VN, today theo giờ ứng dụng và today theo giờ server lệch nhau một ngày → query không khớp. Đây là flaky do thời gian & múi giờ, cực kỳ khó tái hiện vì nó chỉ xảy ra trong một khung giờ hẹp mỗi đêm.
Cách xử lý: mock đồng hồ hệ thống (fix cứng thời điểm) trong test, và luôn khai báo timezone tường minh thay vì dựa vào timezone mặc định của máy. Sau đó thêm một test riêng chạy đúng thời điểm ranh giới ngày để đảm bảo logic đúng.
Bài học: Bất kỳ test nào đụng đến now(), ngày tháng, hay múi giờ đều là ứng viên flaky tiềm tàng. Với đội ngũ VN làm việc trên hạ tầng CI theo UTC, đây là cái bẫy phải chủ động phòng ngừa.
Hướng dẫn từng bước
Đây là quy trình quản lý flaky test có hệ thống mà bạn có thể áp dụng ngay cho team:
Bước 1 — Phát hiện (Detect). Bật cơ chế tự động đánh dấu test flaky. Cách đơn giản nhất: cho CI chạy lại test đã fail (auto-retry) và ghi log những test nào "fail rồi pass" — đó chính là flaky. Nhiều công cụ hỗ trợ sẵn: JUnit có @RepeatedTest, pytest có plugin pytest-rerunfailures (--reruns 2), Cypress/Playwright có tính năng retries built-in. Điểm mấu chốt: retry để phát hiện và ghi nhận, không phải để che giấu.
Bước 2 — Đo lường (Measure). Tính flakiness rate cho từng test: số lần fail-rồi-pass chia tổng số lần chạy. Lưu vào một dashboard (Allure, hoặc bảng đơn giản trong CI). Bạn không thể quản lý cái mình không đo được. Xếp hạng test theo mức độ flaky để biết ưu tiên sửa cái nào trước.
Bước 3 — Cách ly (Quarantine). Với test flaky đã xác định nhưng chưa kịp sửa, chuyển nó ra khỏi luồng chặn merge (gắn tag @flaky hoặc @quarantine). Nó vẫn chạy, vẫn báo cáo, nhưng không làm đỏ build chặn deploy. Điều này bảo vệ niềm tin vào build xanh, đồng thời không xóa test đi mất dấu vết.
> Cảnh báo quan trọng: Quarantine phải là phòng chờ có thời hạn, không phải nhà tù chung thân. Đặt SLA rõ ràng, ví dụ "test trong quarantine quá 2 tuần chưa sửa thì bị xóa hoặc chủ sở hữu phải giải trình".
Bước 4 — Chẩn đoán (Diagnose). Với mỗi test flaky, tái hiện lỗi bằng cách chạy lặp nhiều lần: pytest --count=100 test_x.py hoặc chạy trong vòng lặp shell 50–100 lần. Đồng thời thử chạy đảo thứ tự test (pytest có pytest-randomly) để lộ ra order dependency. Ghi lại log, screenshot, video (Selenium/Cypress/Playwright đều hỗ trợ) tại thời điểm fail.
Bước 5 — Sửa tận gốc (Fix root cause). Dựa vào bảng nguyên nhân ở trên, tấn công đúng gốc rễ. Đừng vá bằng cách tăng sleep — đó là chuyển flaky thành test chậm chứ không hết flaky. Sửa là: thay sleep bằng wait điều kiện, cô lập state, mock dịch vụ ngoài, fix clock.
Bước 6 — Xác minh & đưa trở lại (Verify & un-quarantine). Sau khi sửa, chạy lại 100+ lần để chứng minh nó ổn định, rồi mới gỡ tag quarantine đưa test trở lại luồng chặn merge.
Bước 7 — Phòng ngừa & giao trách nhiệm (Prevent & own). Gán chủ sở hữu cho mỗi test (hoặc mỗi nhóm test). Theo dõi flakiness rate như một chỉ số sức khỏe của suite, review định kỳ trong retro của team.
Lỗi thường gặp & mẹo
Lỗi 1 — Dùng retry như liều thuốc phiện. Bật retry vô tội vạ để build lúc nào cũng xanh. Đây là sai lầm phổ biến và tai hại nhất. Retry chỉ nên dùng để phát hiện flaky (rồi ghi nhận và sửa), không phải để giấu nó vĩnh viễn. Một test cần retry để pass vẫn là test bị bệnh.
Lỗi 2 — Vá bằng sleep cứng. Thấy test flaky là thêm Thread.sleep(3000). Nó có thể làm giảm tần suất fail nhưng: (a) không chữa gốc, máy yếu hơn vẫn fail; (b) làm suite chậm khủng khiếp. Hãy luôn dùng explicit wait chờ điều kiện cụ thể (phần tử xuất hiện, text đúng giá trị) thay vì chờ mù một khoảng thời gian.
Lỗi 3 — Xóa quách test cho khỏe. Test flaky khó chịu quá, thôi xóa. Nhưng nhiều khi test flaky đang cố báo cho bạn một bug race condition thật trong sản phẩm — thứ mà người dùng cũng sẽ gặp ngẫu nhiên. Trước khi xóa, hãy hỏi: "Liệu tính non-deterministic này có phản ánh một vấn đề thật trong ứng dụng không?"
Lỗi 4 — Không cô lập dữ liệu giữa các test. Test dùng chung một record trong DB, chạy song song là đá nhau. Mẹo: mỗi test tự tạo dữ liệu riêng (unique ID, email theo timestamp), và dọn sạch sau khi chạy — lý tưởng nhất là bọc trong transaction rồi rollback.
Lỗi 5 — Bỏ quên yếu tố thời gian và ngẫu nhiên. now(), Math.random(), Faker không seed — đều là mầm mống flaky. Mẹo: cố định (freeze) thời gian và seed random trong môi trường test để kết quả tất định.
Mẹo vàng: Xây một flaky test dashboard ngay từ sớm, dù chỉ là bảng đơn giản. Khi bạn có số liệu "top 10 test flaky nhất tháng này", cuộc nói chuyện với team và sếp trở nên dễ dàng hơn nhiều — bạn có bằng chứng, không phải cảm tính.
Bài tập thực hành
- Tự tạo một flaky test bằng ngôn ngữ bạn quen (Python/Java/JS): viết test có
if random.random() < 0.3: fail. Chạy 20 lần, quan sát tỷ lệ đỏ/xanh. Mục tiêu: cảm nhận trực tiếp sự khó chịu của non-determinism.
- Tái hiện order dependency: viết hai test dùng chung một biến global/một record DB. Test B chỉ pass nếu test A chạy trước. Sau đó dùng công cụ chạy ngẫu nhiên thứ tự (
pytest-randomlyhoặc tự shuffle) để làm nó fail. Rồi sửa bằng cách cho mỗi test tự setup state riêng.
- Đo flakiness rate: lấy một test bất kỳ, chạy nó 100 lần bằng vòng lặp (
pytest --count=100hoặc script shell). Ghi lại số lần pass/fail và tính tỷ lệ flaky. Viết ra file kết quả.
- Thiết kế quy trình quarantine cho team giả định: viết một tài liệu ngắn (1 trang) trả lời: test bị đánh dấu flaky theo tiêu chí nào? Ai chịu trách nhiệm sửa? SLA bao lâu? Điều gì xảy ra khi hết SLA? Đây là kỹ năng "process" mà một SDET giỏi phải có, không chỉ code.
- Săn bẫy múi giờ: viết một test đụng đến ngày "hôm nay", cố tình để nó fail khi giả lập chạy lúc nửa đêm giờ VN. Sau đó sửa bằng cách mock đồng hồ và khai báo timezone tường minh.
Tóm tắt
Flaky test là bài kiểm thử cho kết quả dao động pass/fail dù code không đổi — và tác hại lớn nhất của nó không phải là tốn thời gian, mà là bào mòn niềm tin đến mức team bỏ qua cả những cảnh báo thật.
Những điều cần nhớ:
- Nguyên nhân: chủ yếu là timing/async, shared state, order dependency, thời gian/múi giờ, dữ liệu ngẫu nhiên, và phụ thuộc dịch vụ ngoài. Timing và shared state chiếm phần lớn.
- Quy trình quản lý: Phát hiện → Đo lường → Cách ly (quarantine có SLA) → Chẩn đoán → Sửa tận gốc → Xác minh → Phòng ngừa.
- Nguyên tắc bất di bất dịch: Retry để phát hiện, không phải để che giấu. Không bao giờ vá flaky bằng
sleepcứng. Quarantine là phòng chờ có thời hạn, không phải nơi vứt bỏ. - Bối cảnh VN: cẩn thận với bẫy múi giờ UTC+7 khi CI chạy theo UTC, và với các đợt cao điểm (sale) khi hạ tầng tải nặng làm flaky bùng phát.