CupCode.cc
Section 04 · Module 04 · Lesson 02 · ~15 นาที

Debugging Flaky Tests

เวลาเรียน: 15 นาที ต้องรู้มาก่อน: Module 04, Lesson 01: Auto-waiting รูปแบบ: อ่าน + แบบฝึกสั้น

จบบทนี้แล้วคุณจะ...

  • เข้าใจสาเหตุของ 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();
});

วิธีหาสาเหตุ:

  1. เปิด trace viewer — เห็นว่า notification ปรากฏแล้วหายภายใน 2 วินาที
  2. บาง run notification หายก่อน assertion ทำงานเสร็จ
  3. สาเหตุ: 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 ทำให้อัตโนมัติถ้าใช้ page fixture)

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 อื่น