Flaky test là test khi pass khi fail dù code không đổi. Cái hại lớn nhất không phải bản thân lỗi, mà là team mất niềm tin vào test suite rồi bắt đầu bỏ qua cả những lần fail thật.
Sáu nguyên nhân hay gặp:
1. Timing/async — test không chờ đúng thao tác bất đồng bộ, dùng setTimeout ước chừng thay vì waitFor hoặc await tường minh.
2. Phụ thuộc thứ tự — test A sống nhờ side effect của test B.
3. Shared mutable state — global state không được reset giữa các test.
4. Phụ thuộc bên ngoài — gọi API thật, database không cô lập.
5. Thời gian — dùng new Date() thay vì mock.
6. Dữ liệu ngẫu nhiên — không seed bộ sinh số ngẫu nhiên.
Cách chữa: dùng waitFor(() => expect(element).toBeInTheDocument()) của Testing Library thay setTimeout; reset state trong beforeEach; mock Date.now() bằng jest.useFakeTimers(); gọi server.resetHandlers() giữa các test.
Cách phát hiện: chạy đi chạy lại một file test. Jest không có cờ lặp sẵn nên dùng shell loop:
for i in {1..20}; do npx jest flaky.test.ts --runInBand || break; donePlaywright thì có sẵn --repeat-each.
Lưu ý: retry trong CI chỉ là cơ chế phát hiện, không phải cách chữa — test phải retry mới xanh thì vẫn là test hỏng, và giấu nó đi bằng --retries=5 chỉ dời nợ sang sau.