Chào mọi người, bài này mình sẽ nói về nỗi ám ảnh của bất kì ai làm automation test. Đó chính là flaky test - test lúc pass, lúc fail, mà chẳng ai đụng vào code cả.

Flaky test là loại test mà kết quả không nhất quán: chạy lần này pass, chạy lần khác (với y hệt code, y hệt dữ liệu) lại fail. Đây là một trong những thứ gây mất niềm tin vào automation test nhanh nhất - vì khi test báo fail, không ai biết đó là lỗi thật hay chỉ là "nó vậy đó", dần dần mọi người có xu hướng bấm "chạy lại" thay vì điều tra, và cứ thế bộ test suite mất giá trị.

Trước khi tìm cách sửa, việc đầu tiên cần làm là xác định đúng nguyên nhân - vì mỗi loại flaky cần một cách chữa khác nhau.

Nguyên nhân 1: Timing - trạng thái bất đồng bộ chưa sẵn sàng

Đây là nguyên nhân rất thường gặp. Test đọc hoặc verify trạng thái trước khi ứng dụng cập nhật xong, hoặc tiếp tục flow khi dữ liệu bất đồng bộ chưa sẵn sàng.

// Dễ gây flaky: lấy giá trị ngay trong khi UI vẫn đang update
const status = await page.locator('.status').textContent(); expect(status).toBe('Saved');

Nếu UI vẫn đang cập nhật, tại thời điểm textContent() được lấy ra, status có thể vẫn là "Saving..." hoặc giá trị cũ. Lúc này expect(status).toBe('Saved') không tự retry nên testcase sẽ fail.

  • Cách xử lí: Playwright đã tự động chờ phần tử "actionable" trước khi click/fill, nhưng với các trường hợp phức tạp hơn (chờ dữ liệu load, chờ animation) thì nên chờ đúng điều kiện thay vì chờ "cho chắc"
// Tốt hơn: chờ đúng trạng thái mong đợi 
await expect(page.locator('.status')).toHaveText('Saved');
// Hoặc chờ 1 API cụ thtrả về xong ri mới thao tác tiếp
const responsePromise = page.waitForResponse( resp => resp.url().includes('/api/cart') && resp.status() === 200 );
await page.getByRole('button', { name: 'Add to cart' }).click(); 
await responsePromise;

Tránh dùng trong testcase thực tế:

// Đừng làm thế này - đây chỉ là vá tạm chứ không chữa tận gốc
await page.waitForTimeout(3000);

waitForTimeout giống như uống thuốc giảm đau mà không tìm ra bệnh - hôm nay 3 giây đủ, mai server chậm hơn 1 chút là fail lại. Luôn ưu tiên chờ theo điều kiện (phần tử xuất hiện, API response, ...) thay vì chờ theo thời gian cố định.

Nguyên nhân 2: Test phụ thuộc lẫn nhau

Đây là hệ quả trực tiếp của việc không quản lí dữ liệu test tốt (mình có nói ở bài trước). Ví dụ:

test('creates an order', async ({ page }) => {
  // tạo đơn hàng cho user "user@test.com"
});

test('cancels the latest order', async ({ page }) => {
  // giả định đơn hàng vừa tạo ở test trên vẫn còn đó
});

Nếu 2 test này chạy song song, hoặc thứ tự bị đổi, test thứ hai sẽ fail vì "đơn hàng" nó mong đợi chưa chắc đã tồn tại.

  • Cách xử lí: mỗi test tự tạo dữ liệu riêng cho mình, không dựa vào kết quả để lại từ test khác. Quy tắc: mỗi test phải chạy độc lập, không phụ thuộc thứ tự và nếu có thể thì chạy song song mà không tác động lẫn nhau.

Nguyên nhân 3: Môi trường test không ổn định

Đôi khi flaky không nằm ở code test, mà ở hạ tầng: server dev bị quá tải, network không ổn định, hoặc third-party service (thanh toán, gửi email...) đôi lúc chậm/lỗi.

  • Cách xử lí: với các phần phụ thuộc bên thứ ba không kiểm soát được, hãy cân nhắc mock lại thay vì gọi thật nếu third-party không phải chính integration mà testcase đang muốn kiểm thử (mình sẽ có bài riêng về chủ đề này). Với hạ tầng nội bộ chậm, cần làm việc với team để cải thiện, vì dù test viết tốt cỡ nào cũng không thể ổn định trên một hạ tầng không ổn định.

Nguyên nhân 4: Race condition trong chính ứng dụng

Có những trường hợp flaky không phải lỗi test, mà là bug thật trong ứng dụng - ví dụ 2 request gửi gần như đồng thời gây ra kết quả không xác định. Đây là lúc flaky test lại trở thành có ích: nó đang báo cho bạn biết ứng dụng có vấn đề thật sự, đừng vội "fix" bằng cách thêm wait để che giấu bug đi.

Công cụ: Trace Viewer

Khi không rõ nguyên nhân, đừng đoán mò - hãy bật trace để Playwright ghi lại toàn bộ quá trình chạy test: từng bước thao tác, screenshot, network request, console log.

// playwright.config.ts
export default defineConfig({
  use: {
    trace: 'retain-on-failure', // record trace cho mỗi run, nhưng chỉ giữ lại trace của những run fail
  },
});

Khi test fail, mở trace lên xem lại:

npx playwright show-trace trace.zip

Trace Viewer cho xem lại y hệt như tua lại video: phần tử nào chưa xuất hiện, request nào bị chậm, response trả về gì tại đúng thời điểm test fail. Đây là một trong những công cụ hữu ích nhất để debug flaky test phía browser - đừng bỏ qua nó và vội vàng đoán nguyên nhân.

Lời kết

Flaky test không tự nhiên biến mất, và cũng không nên "sống chung" bằng cách bấm retry mãi mãi. Mỗi lần gặp flaky, hãy dành thời gian dùng Trace Viewer để tìm ra nguyên nhân thật sự - có thể đến từ timing, dữ liệu/shared state, môi trường, dependency hoặc thậm chí là bug thật của ứng dụng. Xử lí đúng gốc rễ sẽ giúp bộ test suite đáng tin cậy hơn, và quan trọng nhất - giúp cả team tin tưởng vào kết quả test một lần nữa. Chúc bạn sớm "bắt được bệnh" của những con test khó chiều!