Mục lục
- Mục Tiêu Bài Học
- Flaky Test Là Gì?
- Step 1 — Identify: Tìm Test Đang Flaky
- Step 2 — Reproduce: Bắt Fail Có Kiểm Soát
- Step 3 — Analyze Trace: Tìm Action Có Vấn Đề
- Step 4 — Root Cause: Phân Loại Nguyên Nhân
- Root Cause Patterns
- Step 5 — Annotate Và Classify Severity
- Tools Diagnose
- Limitation
- Pitfalls
- Quiz
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài này, bạn sẽ:
- Biết cách đọc HTML report tab "Flaky" và extract thông tin cần thiết để bắt đầu investigate.
- Dùng
--repeat-eachđể reproduce flaky test có kiểm soát thay vì chờ CI fail. - Phân tích Trace Viewer: so sánh trace pass vs trace fail để tìm action, network call, hoặc DOM state gây ra sự khác biệt.
- Phân loại root cause vào ít nhất 9 nhóm nguyên nhân phổ biến.
- Annotate test với metadata flaky để tracking và prioritize fix.
- Phân biệt severity (rare / occasional / frequent) để quyết định ưu tiên xử lý.
Flaky Test Là Gì?
Flaky test là test đôi khi pass, đôi khi fail khi chạy với cùng codebase, không phụ thuộc vào thay đổi code. Đây là điểm phân biệt với test fail thông thường:
| Loại | Behavior | Nguyên nhân điển hình |
|---|---|---|
| Failed | Fail mọi lần | Bug trong code, assertion sai |
| Flaky | Pass / fail không nhất quán | Timing, race condition, external dependency |
Trong Playwright, một test được đánh dấu flaky khi nó fail ít nhất 1 lần nhưng pass ở ít nhất 1 lần retry (xem bài 74 về retries). Nếu test fail mọi lần kể cả retry, nó là failed — không phải flaky.
Flaky test gây ra hai vấn đề thực tế:
- Làm mất tin tưởng vào test suite — developer bắt đầu bỏ qua CI red.
- Che khuất bug thật — một flaky test pass cuối cùng nhưng có thể đang che đậy regression thực sự.
Step 1 — Identify: Tìm Test Đang Flaky
HTML Report — Tab Flaky
Khi chạy với retries > 0, HTML report Playwright phân loại kết quả vào các tab: Passed, Failed, Flaky, Skipped. Tab Flaky liệt kê các test fail ít nhất một lần nhưng cuối cùng pass sau retry.
Thông tin cần đọc trong tab Flaky:
- Test name và file path — xác định chính xác test nào.
- Attempts — bao nhiêu lần chạy, lần nào fail, lần nào pass.
- Error message của lần fail —
TimeoutError,locator.clickkhông tìm thấy element, assertion mismatch, v.v. - Trace file đính kèm — nếu đã cấu hình
trace: 'on-first-retry'.
CI Dashboard — Track Flaky Rate
HTML report chỉ cho thấy kết quả một lần chạy. Để biết flaky rate theo thời gian (vd test X fail 3% tổng số lần chạy), cần aggregate từ nhiều CI run. Một số tùy chọn:
- Currents.dev — cloud dashboard tích hợp Playwright, tự động track flaky rate per test, hiển thị trend theo ngày, per-branch.
- GitHub Actions annotations — parse XML report và annotate PR khi test flaky.
- Custom script — parse JSON report (
--reporter=json) và ghi vào metrics system (Datadog, Grafana).
Với team nhỏ, tab Flaky trong HTML report sau mỗi CI run là đủ để identify. Với team lớn chạy nhiều CI pipeline song song, cần aggregate tool để thấy flaky rate tổng thể.
Step 2 — Reproduce: Bắt Fail Có Kiểm Soát
--repeat-each
--repeat-each=N cho Playwright chạy mỗi test N lần trong cùng một run. Đây là cách nhanh nhất để catch flaky test local mà không cần viết loop.
# Chạy test 50 lần để bắt flaky
npx playwright test path/to/flaky.spec.ts --repeat-each=50
# Chạy tất cả test có tag @flaky 20 lần
npx playwright test --grep "@flaky" --repeat-each=20
Nếu test fail ít nhất 1 trong 50 lần, nó xuất hiện trong tab Flaky hoặc Failed của report. Chọn N tùy fail rate ước tính: flaky 1% cần chạy ~200 lần để có 90% xác suất catch được ít nhất 1 lần fail.
Chạy Serial Để Bắt Test Dependency
Flaky test đôi khi chỉ fail khi chạy sau một test cụ thể khác (shared state bị contaminate). Để tái hiện:
# Chạy toàn bộ suite single-threaded, không parallel
npx playwright test --workers=1
# Hoặc grep file chứa test suspect + test trước đó
npx playwright test suspect.spec.ts --workers=1
Nếu test pass khi chạy đơn lẻ nhưng fail khi chạy trong suite với --workers=1, nguyên nhân gần như chắc chắn là shared state từ test trước (global variable, database record, cookie không được cleanup).
Reproduce Đúng CI Environment
Flaky xảy ra trên CI nhưng không reproduce local có thể do khác biệt môi trường: CPU throttle, memory limit, network latency. Để giảm sai lệch:
- Chạy trong Docker container giống image CI (
mcr.microsoft.com/playwright:v1.x.x-focal). - Giới hạn CPU/memory container khi test:
--cpus=2 --memory=4g. - Dùng
--workers=4hoặc số workers giống CI thay vì mặc định.
Step 3 — Analyze Trace: Tìm Action Có Vấn Đề
Bật Trace Khi Flaky
Cấu hình trace: 'on-first-retry' để Playwright tự động lưu trace file khi test fail lần đầu và được retry:
// playwright.config.ts
export default defineConfig({
retries: 2,
use: {
trace: 'on-first-retry',
},
});
on-first-retry có nghĩa: trace được thu thập cho attempt thứ hai — tức lần retry đầu tiên sau attempt 0 fail. Đây là tradeoff giữa thu thập đủ dữ liệu và không làm chậm mọi test pass.
Nếu cần trace cả attempt đầu tiên (lần fail ban đầu), dùng trace: 'retain-on-failure' — lưu trace cho mọi lần fail:
use: {
trace: 'retain-on-failure',
}
Mở Trace Viewer
Sau khi có trace file (.zip trong test-results/):
npx playwright show-trace test-results/path-to/trace.zip
Hoặc mở trực tiếp từ HTML report: click vào test flaky → click "Trace" trong panel details.
Các Panel Cần Phân Tích
Action log (timeline): Danh sách từng action Playwright thực hiện theo thứ tự thời gian. Tìm action nào có:
- Duration bất thường cao so với các lần chạy khác.
- Error indicator (icon đỏ hoặc timeout message).
- Action bị stuck:
locator.waitForchờ đến hết timeout.
Network panel: Xem các HTTP request trong khoảng thời gian quanh action fail:
- Request nào trả về 5xx, 4xx không mong đợi?
- Request nào có latency đột biến (vd bình thường 50ms nhưng lần fail là 3000ms)?
- Request nào bị cancel hoặc không có response?
Snapshot panel: DOM state tại thời điểm action. So sánh snapshot của trace fail với trace pass:
- Element có hiển thị trong DOM không? Hay bị ẩn (
display: none,visibility: hidden)? - Element có đang trong animation/transition?
- Nội dung text hay attribute khác nhau giữa hai trace?
Console panel: JavaScript console log và error từ browser:
- Uncaught exception trong application code?
- Failed fetch / XHR với error message cụ thể?
- Warning về race condition từ React, Vue hay framework đang dùng?
Kỹ Thuật So Sánh Trace Pass vs Fail
Nếu có trace của cả attempt pass và attempt fail, mở cả hai trong Trace Viewer rồi so sánh:
- Tìm action đầu tiên có timeline diverge — thời điểm hai trace bắt đầu khác nhau.
- Xem network activity trước action đó: có request nào chậm hơn bình thường?
- Xem snapshot tại action đó: DOM state có khác không?
Điểm diverge đầu tiên thường chỉ đúng vào root cause, dù không phải 100% trường hợp.
Step 4 — Root Cause: Phân Loại Nguyên Nhân
Sau khi analyze trace, đặt câu hỏi: "Tại sao điều này xảy ra không nhất quán?" Câu trả lời thường rơi vào một trong các nhóm sau:
Timeout Pattern
Dấu hiệu: Error message là TimeoutError: locator.click hoặc waitForSelector exceeded. Action này pass nhanh trong trace bình thường nhưng chờ đến timeout trong trace fail.
Nguyên nhân: Action chạy trước khi resource sẵn sàng — element chưa render, data API chưa load, animation chưa kết thúc.
Phân biệt: Nếu test luôn fail với timeout → bug thật. Nếu fail không nhất quán → race condition hoặc timing issue.
Network Pattern
Dấu hiệu: Network panel trong trace fail có request trả về 5xx, timeout, hoặc latency đột biến. Application code phụ thuộc kết quả request đó và không handle error case.
Nguyên nhân: API service không ổn định, database connection pool hết, third-party service có downtime ngắn.
State Pattern
Dấu hiệu: Test chỉ fail khi chạy sau một test cụ thể khác. Pass khi chạy đơn lẻ.
Nguyên nhân: Test trước để lại shared state: global variable, record trong database, cookie, localStorage, hay side effect trong application state.
Browser Pattern
Dấu hiệu: Test fail chỉ trên Firefox (hoặc WebKit, Chromium) nhưng pass trên các engine còn lại.
Nguyên nhân: Rendering engine khác nhau xử lý animation, font, layout hay event timing khác. Đây thường không phải flaky thực sự mà là browser-specific bug.
Root Cause Patterns — 9 Nhóm Phổ Biến
| # | Root Cause | Dấu Hiệu Trong Trace |
|---|---|---|
| 1 | Race condition | Action timeout, element không ready khi click |
| 2 | Network flakiness | API request 5xx hoặc timeout trong network panel |
| 3 | Animation timing | Snapshot cho thấy element đang transition/opacity change |
| 4 | Random data | Assertion fail vì thứ tự sort, ID hay timestamp thay đổi |
| 5 | Shared state | Fail sau test X; pass khi chạy đơn lẻ |
| 6 | Resource contention | Fail khi chạy parallel, pass khi --workers=1 |
| 7 | Time-dependent | Fail vào giờ nhất định (UTC offset, timezone, end-of-day) |
| 8 | External service | Third-party API (email, SMS, OAuth) không phản hồi kịp |
| 9 | Browser rendering | Fail chỉ một browser engine; snapshot có pixel artifact |
Cách Dùng Bảng Này
Sau khi analyze trace, match dấu hiệu vào bảng để xác định nhóm. Một flaky test có thể có nhiều nguyên nhân cùng lúc — vd race condition kết hợp resource contention trên CI. Trong trường hợp đó, xử lý nguyên nhân primary trước (thường là race condition hoặc shared state).
Race Condition — Đào Sâu
Race condition trong E2E test thường xuất hiện khi:
- Test click button ngay sau navigate nhưng JavaScript bundle chưa hydrate xong.
- Test fill form ngay sau clear nhưng framework (React, Vue) chưa re-render.
- Test assert text ngay sau API call nhưng UI chưa update.
Dấu hiệu điển hình: trace fail có action duration đột ngột dài (wait đến timeout), trong khi trace pass action đó hoàn thành trong <100ms.
Shared State — Đào Sâu
Shared state phức tạp hơn vì không phải lúc nào cũng hiển thị trong trace. Một số trường hợp hay gặp:
- Test A tạo record trong DB, test B đọc danh sách và assert số lượng — nếu test A và B chạy song song, số lượng không nhất quán.
- Test A đăng nhập và lưu auth token vào localStorage, test B chạy trong cùng browser context → inherit auth state không mong đợi.
- Test A thay đổi global config qua API mà không cleanup → test B chạy với config sai.
Step 5 — Annotate Và Classify Severity
Annotation Flaky
Khi đã identify root cause nhưng chưa fix được ngay (đang track bug, external service issue, v.v.), annotate test để ghi lại thông tin:
test('checkout flow completes', {
annotation: {
type: 'flaky',
description: 'Race condition with payment API — API có lag không nhất quán, issue #1234',
},
}, async ({ page }) => {
// ... test body
});
Annotation type: 'flaky' xuất hiện trong HTML report dưới phần annotations của test đó. Không ảnh hưởng đến kết quả test — test vẫn pass/fail bình thường.
Kết hợp với tag để dễ grep:
test('checkout flow completes', {
tag: ['@flaky'],
annotation: {
type: 'flaky',
description: 'Race condition with payment API — issue #1234',
},
}, async ({ page }) => {
// ...
});
# Chạy riêng các test được đánh dấu flaky để monitor
npx playwright test --grep "@flaky" --repeat-each=30
Severity Classification
Phân loại severity dựa trên fail rate để quyết định ưu tiên:
| Severity | Fail Rate | Hành Động |
|---|---|---|
| Rare | < 1% | Annotate, monitor, không urgent |
| Occasional | 1–10% | Prioritize trong sprint hiện tại |
| Frequent | > 10% | Fix ngay — đang làm nhiễu CI liên tục |
Fail rate tính bằng: (số lần fail) / (tổng số lần chạy) × 100%. Cần ít nhất 50–100 lần chạy để có số liệu đủ tin cậy.
Git Bisect Cho Flaky Mới Xuất Hiện
Nếu flaky mới xuất hiện sau một khoảng thời gian nhất định, có thể dùng git bisect để tìm commit gây ra:
git bisect start
git bisect bad HEAD
git bisect good <commit-hash-trước-khi-flaky>
# Với mỗi commit bisect trỏ đến, chạy test nhiều lần
npx playwright test path/to/flaky.spec.ts --repeat-each=20
# Mark kết quả
git bisect good # hoặc
git bisect bad
Phương pháp này hiệu quả khi flaky có fail rate cao (>10%) — chạy 20 lần đủ để detect. Với fail rate thấp cần nhiều lần hơn, bisect mất nhiều thời gian hơn.
Tools Diagnose
| Tool | Dùng Khi Nào | Lệnh / Cách Dùng |
|---|---|---|
| Trace Viewer | Analyze per-action sau khi catch fail | npx playwright show-trace trace.zip |
| --debug | Step-through interactive khi có thể reproduce | npx playwright test --debug |
| UI Mode | Watch mode, replay test tương tác | npx playwright test --ui |
| --repeat-each | Reproduce flaky bằng cách lặp lại nhiều lần | --repeat-each=50 |
| --workers=1 | Bắt shared state dependency | --workers=1 |
| CI logs | So sánh environment: CPU, memory, OS | Xem job log trong GitHub Actions / Jenkins |
Kết Hợp Tools
Workflow điển hình khi nhận được báo cáo flaky từ CI:
- Đọc error message trong HTML report tab Flaky → biết action nào fail.
- Mở trace file đính kèm → xem action log + network + snapshot.
- Nếu chưa rõ, chạy
--repeat-each=30local để reproduce. - Nếu vẫn không reproduce, chạy trong Docker container giống CI.
- Nếu nghi shared state, thêm
--workers=1và--repeat-each=10.
Limitation
Trước khi dành nhiều thời gian diagnose, cần biết các giới hạn của phương pháp này:
Reproduce Local Không Phải Lúc Nào Cũng Khả Thi
Một số flaky test chỉ xảy ra trên CI do tổ hợp đặc biệt: hardware cụ thể, OS version, browser binary khác, network latency từ external service. Reproduce local không thể tái tạo hoàn toàn môi trường đó. Trong trường hợp này, trace từ CI run là dữ liệu duy nhất có thể dựa vào.
Trace Chỉ Cover Playwright-Level
Trace Viewer ghi lại mọi action Playwright thực hiện và network request của browser. Nó không capture:
- System-level noise: CPU spike, memory pressure, disk I/O.
- Server-side behavior: database query plan thay đổi, background job chạy cùng lúc.
- Network hop ở tầng infrastructure (DNS, load balancer).
Nếu root cause nằm ở tầng này, trace không đủ — cần server-side logging và infrastructure metrics.
Root Cause Có Thể Bên Ngoài App
Flaky do external service (OAuth provider, payment gateway, email API) là trường hợp không thể fix ở test level. Giải pháp duy nhất là mock service đó trong test, hoặc tách thành integration test chạy riêng với retry cao hơn.
Sai Lầm False Positive
Chạy --repeat-each=20 không fail không có nghĩa test stable. Fail rate 1% cần ~200 lần chạy để detect. Không reproduce ≠ không flaky.
Pitfalls
1. Kết Luận "Flaky" Mà Không Investigate Root Cause
Khi test pass sau retry, developer đôi khi chấp nhận luôn và bỏ qua. Đây là sai lầm: flaky test không tự khỏi, fail rate có xu hướng tăng dần khi codebase phức tạp hơn. Cần investigate ngay cả khi fail rate chỉ 2%.
2. Fix Flaky Bằng Cách Tăng Timeout
Tăng timeout global từ 30s lên 60s làm test "ít fail hơn" nhưng không fix root cause. Test vẫn flaky, chỉ mất nhiều thời gian hơn để fail. CI duration tăng, và khi service thực sự chậm, test vẫn fail.
// Đây là mask, không phải fix
test.setTimeout(60_000); // BAD: che giấu race condition
// Fix đúng: wait đúng condition
await expect(page.locator('[data-loaded]')).toBeVisible(); // GOOD
3. Reproduce Local Rồi Kết Luận Không Phải Flaky
Không reproduce local ≠ test stable. CI có environment khác — ít CPU, nhiều concurrent workers, network có latency hơn. Phải dùng trace từ CI run làm bằng chứng, không phải kết quả chạy local.
4. Quên Kiểm Tra Shared State Across Tests
Nhiều người chỉ nhìn vào test bị fail mà không xem test nào chạy trước đó. Chạy --workers=1 và để ý output log để xem thứ tự test — đây là bước cần làm song song với phân tích trace.
Quiz
Câu 1. Bạn muốn bắt một flaky test có fail rate khoảng 5% bằng cách chạy nhiều lần local. Số lần chạy tối thiểu nào cho bạn xác suất >90% bắt được ít nhất 1 lần fail?
Xem đáp án
Xác suất ít nhất 1 fail sau N lần = 1 - (0.95)^N. Cần 1 - (0.95)^N > 0.9, tức (0.95)^N < 0.1. Giải ra N > 44. Vậy cần chạy ít nhất 45 lần (--repeat-each=45).
Câu 2. Trace Viewer của lần fail cho thấy locator.click() mất 29.8s (gần bằng timeout 30s), trong khi trace pass thực hiện trong 120ms. Network panel sạch — không có request nào chậm. DOM snapshot tại thời điểm đó cho thấy button có class animate-spin. Root cause thuộc nhóm nào?
Xem đáp án
Animation timing. Button đang trong trạng thái animation (animate-spin), Playwright chờ element "stable" (không di chuyển) trước khi click. Nếu animation không kết thúc, action timeout. Nhóm 3 trong bảng root cause.
Câu 3. Test user-list.spec.ts pass khi chạy đơn lẻ nhưng fail khi chạy cùng suite với --workers=1. Error: Expected 5 users, received 6. Bước diagnose tiếp theo là gì?
Xem đáp án
Đây là dấu hiệu shared state. Bước tiếp theo: xem output log để tìm test nào chạy ngay trước user-list.spec.ts trong suite. Test đó nhiều khả năng tạo thêm 1 user trong DB mà không cleanup sau khi xong. Kiểm tra afterEach / afterAll của test trước để tìm thiếu cleanup.
Câu 4. Tại sao trace: 'on-first-retry' không capture được trace của lần chạy đầu tiên (attempt 0)?
Xem đáp án
Vì on-first-retry kích hoạt trace bắt đầu từ lần retry đầu tiên (attempt 1), không phải attempt 0. Playwright chỉ biết cần trace sau khi attempt 0 fail. Để có trace của attempt 0, dùng trace: 'retain-on-failure' — lưu trace cho mọi lần fail bất kể là retry hay không.
Câu 5. Một test có fail rate 0.5% trên CI. Bạn nên làm gì?
Xem đáp án
Fail rate <1% → severity Rare. Hành động phù hợp: annotate test với annotation: { type: 'flaky', description: '...' } và tag @flaky để tracking, sau đó monitor xem fail rate có tăng không. Không cần fix ngay nhưng không được bỏ qua hoàn toàn.
Bài Tiếp Theo
Bài 79: Patterns Chống Flaky — các kỹ thuật fix từng nhóm root cause đã phân loại ở bài này: proper wait strategies, isolate test state, mock external service, handle animation.
