Debugging Flaky Tests
จบบทนี้แล้วคุณจะ...
- เข้าใจสาเหตุของ flaky tests ที่พบบ่อย
- ใช้เครื่องมือ debug ของ Playwright: trace viewer, screenshots, video
- กลยุทธ์ retry และ quarantine
- วิเคราะห์ flaky test scenario และหาวิธีแก้
1 · Flaky Test คืออะไร?
Flaky test คือ test ที่บางครั้งผ่าน บางครั้งไม่ผ่าน โดยที่ code ไม่ได้เปลี่ยน เป็นปัญหาที่ร้ายแรงเพราะ:
- ทำลายความเชื่อมั่น — ทีมเริ่มไม่สนใจ test results
- เสียเวลา — ต้อง retry CI pipeline ซ้ำแล้วซ้ำเล่า
- ซ่อนบั๊กจริง — เมื่อ test fail คนจะคิดว่า "แค่ flaky" แทนที่จะตรวจสอบ
2 · สาเหตุของ Flaky Tests
Timing Issues (พบบ่อยที่สุด)
// ปัญหา: element ยังไม่โหลด
await page.waitForTimeout(1000); // อาจไม่พอ
await page.locator('#result').click();
// แก้: ใช้ auto-waiting
await page.getByTestId('result').click();
Shared State ระหว่าง Tests
// ปัญหา: test 2 depend on data จาก test 1
test('should create item', async ({ page }) => {
// สร้าง item ชื่อ "Test Item"
});
test('should delete item', async ({ page }) => {
// ลบ "Test Item" — ถ้า test 1 fail test นี้ก็ fail
});
// แก้: แต่ละ test สร้าง data ของตัวเอง
test('should delete item', async ({ page }) => {
// สร้าง item ก่อน
// แล้วค่อยลบ
});
External Dependencies
- API ภายนอกที่ช้าหรือ down
- Database ที่ช้าเมื่อรัน parallel
- Third-party services (payment gateway, email service)
Animation / Transition
// ปัญหา: คลิกระหว่าง animation
await page.getByRole('button', { name: 'Menu' }).click();
// modal กำลัง animate เข้ามา
await page.getByRole('button', { name: 'Delete' }).click();
// แก้: Playwright auto-wait "stable" check ช่วยได้
// แต่ถ้า animation ซับซ้อนอาจต้อง waitFor
await page.getByRole('dialog').waitFor({ state: 'visible' });
await page.getByRole('button', { name: 'Delete' }).click();
3 · เครื่องมือ Debug ของ Playwright
Trace Viewer — ดูย้อนหลังว่า test ทำอะไร
// playwright.config.ts
use: {
trace: 'on-first-retry', // บันทึก trace เมื่อ retry
}
# ดู trace
npx playwright show-trace trace.zip
Trace viewer แสดง:
- Timeline ของทุก action
- Screenshot ทุก step
- Network requests
- Console logs
- DOM snapshot ที่แต่ละจุด
Screenshots — ภาพหน้าจอเมื่อ fail
// playwright.config.ts
use: {
screenshot: 'only-on-failure',
}
Video — บันทึกวิดีโอทุก test
// playwright.config.ts
use: {
video: 'on-first-retry',
}
4 · Retry Strategy
// playwright.config.ts
export default defineConfig({
// retry 2 ครั้งใน CI
retries: process.env.CI ? 2 : 0,
});
Retry ช่วยอะไร?
- ลดผลกระทบของ flaky test ใน CI
- trace ที่บันทึกตอน retry ช่วย debug
Retry ไม่ได้แก้ปัญหา
- Retry เป็นแค่ band-aid ไม่ใช่ solution
- ต้องหาสาเหตุจริงและแก้ที่ root cause
5 · Quarantine Pattern
เมื่อเจอ flaky test ที่แก้ไม่ได้ทันที:
// ใส่ tag เพื่อแยก flaky test
test('flaky test @flaky', async ({ page }) => {
// ...
});
# รัน test ปกติ (ไม่รวม flaky)
npx playwright test --grep-invert @flaky
# รัน เฉพาะ flaky test (วิเคราะห์)
npx playwright test --grep @flaky
6 · เรื่องเล่า: Test ที่ผ่าน 9/10 ครั้ง
ทีมหนึ่งมี test ที่ตรวจการแสดง notification หลัง submit form ผ่าน 9 ใน 10 ครั้ง:
test('should show success notification', async ({ page }) => {
await page.goto('/form');
await page.getByLabel('Name').fill('John');
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('Saved!')).toBeVisible();
});
วิธีหาสาเหตุ:
- เปิด trace viewer — เห็นว่า notification ปรากฏแล้วหายภายใน 2 วินาที
- บาง run notification หายก่อน assertion ทำงานเสร็จ
- สาเหตุ: notification มี auto-dismiss timer 2 วินาที
วิธีแก้:
// เพิ่ม assertion ที่ตรวจ notification ทันทีหลัง click
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByRole('alert')).toContainText('Saved!');
เปลี่ยนจาก getByText เป็น getByRole('alert') เพราะ notification มี role="alert" ซึ่ง Playwright จะจับได้เร็วกว่า
แบบฝึกหัด
วิเคราะห์ flaky test scenarios แล้วระบุสาเหตุและวิธีแก้:
Scenario 1
Test ตรวจจำนวนสินค้าในตะกร้า — ผ่านเมื่อรัน headed แต่ fail เมื่อรัน headless
ดูคำตอบแนะนำ
- สาเหตุ: headed mode ช้ากว่า เลยมีเวลาให้ UI อัปเดต headless รันเร็วกว่าจน UI ยังไม่ทัน
- แก้: ใช้
await expect(page.getByRole('listitem')).toHaveCount(3)ซึ่งเป็น auto-retry assertion
Scenario 2
Test login ที่รันผ่านเดี่ยวๆ แต่ fail เมื่อรันทั้ง suite
ดูคำตอบแนะนำ
- สาเหตุ: shared state — test อื่นอาจ login/logout ทำให้ session ชน
- แก้: ให้แต่ละ test ใช้ browser context แยก (Playwright ทำให้อัตโนมัติถ้าใช้
pagefixture)
Scenario 3
Test ที่คลิก dropdown แล้ว fail เป็นบางครั้ง พร้อม error "element is not stable"
ดูคำตอบแนะนำ
- สาเหตุ: dropdown มี animation ที่ยังไม่จบ
- แก้:
await page.getByRole('listbox').waitFor({ state: 'visible' })ก่อนคลิก option
สรุปสิ่งที่ต้องจำ
- Flaky test เกิดจาก: timing, shared state, external dependencies, animation
- Trace viewer คือเครื่องมือ debug ที่ดีที่สุด — ดูทุก step ย้อนหลังได้
- ตั้ง
screenshot: 'only-on-failure'และtrace: 'on-first-retry'ใน config - Retry เป็น band-aid ไม่ใช่ solution — ต้องหาและแก้ root cause
- ใช้ quarantine pattern แยก flaky test ออกจาก main suite ชั่วคราว
- ทุก test ต้อง independent — ไม่ depend on state จาก test อื่น