Mục lục
- Mục Tiêu Bài Học
- Nhóm A.8 — Retries & Flaky
- retries: N — Cú Pháp Và Giá Trị Mặc Định
- Behavior Khi Retry
- Worker Recycle Khi Retry
- Pattern CI vs Local
- Override Per-Project
- Override Per-Describe
- testInfo.retry Trong Test
- CLI Override
- Retry và Timeout
- Use Case Nên Và Không Nên Retry
- Anti-Pattern
- --fail-on-flaky-tests (v1.45+)
- Pitfalls
- Quiz
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài này, bạn sẽ:
- Hiểu
retrieshoạt động như thế nào: retry scope, fresh context, trạng thái flaky vs failed. - Biết cách worker được recycle khi retry và hệ quả với
workerIndex. - Viết pattern CI vs local phân biệt rõ mục đích của
retriesở hai môi trường. - Override
retriestheo từng project và từng describe block. - Dùng
testInfo.retryđể branch logic trong test khi đang ở lần retry. - Hiểu
--fail-on-flaky-testsflag (v1.45+) và vai trò của nó. - Nhận ra 4 anti-pattern và pitfall phổ biến khi cấu hình retry.
Lưu ý phạm vi: Override retries qua test.describe.configure() đã được đào sâu ở bài 39. Bài này tập trung vào config-level retries, CLI flag, worker recycle, và toàn bộ use case trong pipeline CI. Test outcomes (flaky, failed, skipped) có bài riêng (bài 75). --fail-on-flaky-tests được giới thiệu qua ở đây và deep dive ở bài 77.
Nhóm A.8 — Retries & Flaky
A.8 đào sâu cách xử lý flaky tests: retries config, outcomes, --fail-on-flaky-tests v1.45, diagnose patterns.
Các bài trong nhóm:
- retries: N (bài này) — cấu hình số lần retry, worker recycle, pattern CI vs local.
- Test outcomes (bài 75) — phân biệt passed, failed, flaky, skipped, interrupted.
- Diagnose flaky (bài 76) — kỹ thuật tìm nguyên nhân: trace, --repeat-each, screenshot on retry.
- --fail-on-flaky-tests (bài 77) — buộc CI fail khi có flaky test, cách tích hợp vào pipeline.
Flaky test là test cho kết quả không nhất quán khi chạy lại với cùng code — pass lần này, fail lần khác. retries là cơ chế giảm noise từ flaky, không phải giải pháp dứt điểm.
retries: N — Cú Pháp Và Giá Trị Mặc Định
retries nhận một số nguyên không âm. Giá trị mặc định là 0 — không retry.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: 2, // Retry tối đa 2 lần khi test fail
});
Semantics: retries: N nghĩa là "chạy lại tối đa N lần sau lần chạy đầu tiên". Tổng số lần chạy tối đa = N + 1.
| retries | Tổng lần chạy tối đa | Ghi chú |
|---|---|---|
0 (mặc định) |
1 | Không retry. Test fail → fail ngay. |
1 |
2 | Retry 1 lần. Fail lần đầu → chạy lần 2. |
2 |
3 | Retry 2 lần. Cấu hình phổ biến nhất trên CI. |
3 |
4 | Dùng cho test cực kỳ flaky do external service. |
Nếu test pass ở lần chạy đầu tiên, Playwright không retry dù config retries: 5. Retry chỉ kích hoạt khi test fail.
Behavior Khi Retry
Khi một test fail, Playwright quyết định có retry hay không dựa trên số lần đã chạy so với retries. Mỗi lần retry là một lần chạy độc lập — context, page, fixture đều được khởi tạo lại từ đầu.
Hai trạng thái kết quả khi dùng retries:
- flaky — fail ít nhất 1 lần nhưng pass ở lần retry. Playwright đánh dấu test là "flaky" trong report.
- failed — fail mọi lần chạy (kể cả tất cả retry). Test được đánh dấu "failed".
Sơ đồ trạng thái với retries: 2:
Lần chạy 1 (attempt 0)
├── PASS → kết quả: passed (không retry)
└── FAIL → retry
Lần chạy 2 (attempt 1)
├── PASS → kết quả: FLAKY
└── FAIL → retry
Lần chạy 3 (attempt 2)
├── PASS → kết quả: FLAKY
└── FAIL → kết quả: FAILED (hết retry)
Retry per-test, không phải per-suite: Mỗi test có quota retry riêng. Một test fail và retry không ảnh hưởng đến test khác trong suite. Test B chạy bình thường dù test A đang ở retry lần 2.
Fresh context mỗi retry: Browser context, cookies, localStorage, service worker — tất cả reset về trạng thái ban đầu trước mỗi lần retry. Nếu fixture có teardown (cleanup sau await use()), teardown chạy sau lần fail và setup chạy lại trước lần retry.
Worker Recycle Khi Retry
Khi một test fail và cần retry, Playwright spawn worker mới thay vì tái dùng worker cũ. Đây là thiết kế cố ý để đảm bảo clean state hoàn toàn.
Hệ quả với workerIndex và parallelIndex:
workerIndex— tăng lên khi worker mới được spawn. Lần đầu dùng worker 0, retry dùng worker 1 (hoặc worker tiếp theo còn trống). Giá trị này không ổn định giữa các lần retry.parallelIndex— đại diện cho slot song song trong suite run, không thay đổi khi retry. Test vẫn chiếm cùng slot, chỉ worker process thay đổi.
test('demo retry worker', async ({}, testInfo) => {
console.log('workerIndex:', testInfo.workerIndex);
// Lần chạy 1: workerIndex = 0
// Retry 1: workerIndex = 2 (worker mới)
// Retry 2: workerIndex = 4 (worker mới)
console.log('parallelIndex:', testInfo.parallelIndex);
// Luôn là 0 (slot không đổi)
});
Worker-scope fixture bị reset khi retry: Nếu bạn dùng worker-scope fixture để cache auth token hoặc DB connection, fixture đó sẽ chạy lại trên worker mới. Đây là behavior đúng — clean state — nhưng có thể làm tăng thời gian retry nếu setup worker-scope fixture tốn thời gian (vd login, seed DB).
Spawn overhead: Mỗi worker mới mất ~200–500ms để khởi động (Node.js process + browser launch). Với retries: 3 và nhiều test flaky, overhead này cộng dồn đáng kể vào CI duration.
Pattern CI vs Local
Pattern được dùng phổ biến nhất trong các project thực tế:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: process.env.CI ? 2 : 0,
});
Logic phân biệt:
- CI (
retries: 2) — môi trường CI có nhiều nguồn noise: shared runner load, network latency, DNS resolution không ổn định. Retry 2 lần giúp pipeline không fail vì một lần fluke. Test fail 3 lần liên tiếp mới thực sự fail. - Local (
retries: 0) — khi dev chạy test trên máy, flaky test cần lộ ra ngay để được fix. Nếu retry ở local, dev không biết test mình vừa viết là flaky.
Lý do giữ local ở 0 có tầm quan trọng thực tế: nếu test chỉ fail trên CI (do retry che đi) mà không fail local, dev không có cơ hội phát hiện sớm. Flaky tích lũy dần thành technical debt khó gỡ.
Lưu ý: pattern này không yêu cầu set cứng 2 cho CI. Có project dùng process.env.CI ? 1 : 0 (chỉ retry 1 lần, ít overhead hơn) hoặc đặt qua env var riêng:
retries: process.env.PLAYWRIGHT_RETRIES
? parseInt(process.env.PLAYWRIGHT_RETRIES, 10)
: process.env.CI ? 2 : 0,
Override Per-Project
Khi config có nhiều project, retries trong mỗi project override giá trị global:
// playwright.config.ts
export default defineConfig({
retries: process.env.CI ? 2 : 0, // Global default
projects: [
{
name: 'stable',
// retries không khai báo → dùng global (2 trên CI, 0 local)
},
{
name: 'flaky-network',
retries: 5, // Override — project này cần nhiều retry hơn
},
{
name: 'integration',
retries: 0, // Override — fail fast, không retry
},
],
});
Khi nào dùng per-project override:
- Project test external service với SLA không đảm bảo →
retries: 3–5. - Project smoke test cần fail nhanh để block deploy →
retries: 0. - Project mobile emulation trên CI runner yếu →
retries: 2dù global là0.
Ưu tiên áp dụng: Project-level retries thắng global retries. Describe-level (bài 39) thắng project-level. CLI --retries thắng tất cả.
Override Per-Describe
Bài 39 đã đào sâu test.describe.configure({ retries }). Ở đây chỉ ghi nhắc cú pháp để hoàn chỉnh bức tranh hierarchy:
// Áp dụng retries: 3 cho tất cả test trong describe block này
test.describe('Flaky external API', () => {
test.describe.configure({ retries: 3 });
test('fetch user data', async ({ request }) => {
// ...
});
test('post order', async ({ request }) => {
// ...
});
});
Override per-describe không cần sửa config, phù hợp khi chỉ một nhóm test nhỏ cần retry khác biệt. Tuy nhiên, nên ưu tiên per-project override nếu tất cả test trong project đều cần cùng giá trị — rõ ràng hơn và dễ maintain hơn.
testInfo.retry Trong Test
testInfo.retry là số nguyên không âm — số lần retry hiện tại. Lần chạy đầu tiên là 0, retry đầu là 1, retry thứ hai là 2.
test('order checkout', async ({ page }, testInfo) => {
if (testInfo.retry > 0) {
console.log(`Retry attempt ${testInfo.retry} of ${testInfo.project.retries}`);
// Có thể thêm cleanup hoặc reset state trước retry
await page.goto('/cart/clear');
}
await page.goto('/checkout');
await page.fill('#card-number', '4111111111111111');
await page.click('#submit');
await expect(page.locator('.success')).toBeVisible();
});
Use case thực tế của testInfo.retry:
- Log thêm thông tin debug chỉ khi đang ở retry (tránh noise trên pass).
- Reset trạng thái phía server trước retry (gọi API cleanup endpoint).
- Bỏ qua cache hoặc thêm delay chỉ ở retry lần 2 trở đi.
- Thêm screenshot thủ công ở retry để dễ so sánh với lần đầu fail.
testInfo.project.retries — cho biết số retry tối đa được config cho project này. Dùng cùng testInfo.retry để biết còn bao nhiêu lần retry:
const retriesLeft = testInfo.project.retries - testInfo.retry;
console.log(`Còn ${retriesLeft} lần retry`);
CLI Override
Flag --retries trên CLI luôn thắng config, kể cả per-project override:
# Chạy với retries: 3, bất kể config khai báo gì
npx playwright test --retries=3
# Disable retry tạm thời dù config có retries: 2
npx playwright test --retries=0
# Kết hợp với project filter
npx playwright test --project=stable --retries=5
Thứ tự ưu tiên đầy đủ (từ cao xuống thấp):
- CLI
--retries=N test.describe.configure({ retries: N })(per-describe)- Project-level
retriestrongprojects[] - Global
retriestrongdefineConfig() - Default:
0
CLI override đặc biệt hữu ích khi debug: chạy --retries=0 để test fail ngay và xem error message đầu tiên mà không chờ các retry sau.
Retry Và Timeout
Mỗi lần retry chạy test từ đầu với full timeout mới. Timeout không cộng dồn giữa các lần chạy.
Test timeout: 30s
retries: 2
Lần 1: chạy 30s → fail (timeout hoặc assertion)
Lần 2 (retry 1): bắt đầu lại, 30s timeout mới
Lần 3 (retry 2): bắt đầu lại, 30s timeout mới
Tổng wall-time tối đa (worst case):
30s × 3 lần = 90s per test
Đây là điểm quan trọng khi ước tính CI duration. Nếu suite có 100 test, mỗi test có timeout 30s, và retries: 2:
- Best case (0 fail): 100 × 30s / workers
- Worst case (100% fail mọi retry): 100 × 90s / workers
Trong thực tế, chỉ một phần nhỏ test là flaky. Nhưng nếu flaky rate cao (10%+ test flaky), retries: 2 có thể thêm 20–30% vào tổng CI time. Đây là lý do không nên tăng retries lên cao — chi phí thời gian tăng tuyến tính.
Lưu ý timeout riêng cho action: actionTimeout và navigationTimeout cũng reset về full giá trị mỗi retry. Không có "tích lũy" timeout nào giữa các lần chạy.
Use Case Nên Và Không Nên Retry
Nên dùng retry:
| Tình huống | Lý do retry hợp lý |
|---|---|
| External API timeout sporadically | Server bên ngoài có SLA không tuyệt đối. Test retry khi API trả 503 nhất thời. |
| CI runner shared load | GitHub Actions free runner có CPU spike không kiểm soát. Animation, timing assertion dễ fail. |
| Browser race condition đã biết | Race condition hiếm gặp trong render cycle, chưa fix được ở app. Retry giảm noise trong pipeline trong thời gian chờ fix. |
| DNS / network resolve không ổn định | Test environment staging có DNS TTL ngắn. Retry thường giải quyết được. |
Không nên dùng retry:
| Tình huống | Lý do retry không phù hợp |
|---|---|
| Test broken do logic sai | Retry không fix bug — chỉ làm kết quả delay. Test sẽ fail mọi lần. |
| Test phụ thuộc thứ tự | Test B cần state do test A tạo ra. Retry test B từ đầu → state không tồn tại → fail mọi lần. |
| Performance / load test | Retry khi vượt threshold thời gian mask regression thực sự. Nên fail fast và điều tra. |
| Test ghi dữ liệu vào DB không idempotent | Retry tạo duplicate records. Cần fix test để idempotent hoặc dùng fixture cleanup trước retry. |
Anti-Pattern
Anti-pattern 1: retries: 10 toàn project
Giá trị retry cao che đậy flaky thực sự. Test pass sau lần retry 7 không đồng nghĩa test ổn — nghĩa là test này cực kỳ flaky và cần được ưu tiên fix. Report sẽ hiện "flaky" nhưng dễ bị bỏ qua khi pipeline xanh.
Anti-pattern 2: Bật retry để che lỗi timeout quá thấp
Nếu test fail vì timeout 5s quá ngắn cho một operation cần 8s, retry không giúp ích — test sẽ timeout mọi lần. Giải pháp đúng là điều chỉnh timeout, không phải tăng retries.
Anti-pattern 3: Pass sau 9 retry → coi là "passed"
Trong pipeline CI, test flaky (pass sau retry) thường được coi là thành công vì pipeline xanh. Đây là nguồn technical debt tích lũy. Test pass sau 9 retry = test có xác suất fail 90% — không nên coi là "healthy".
Flag --fail-on-flaky-tests (bài 77) giải quyết vấn đề này bằng cách fail CI ngay khi phát hiện test flaky dù cuối cùng pass.
Anti-pattern 4: Retry che vấn đề môi trường CI cần fix
Nếu toàn bộ test flaky trên CI nhưng pass local, vấn đề là môi trường (runner overloaded, network unreliable, clock skew). Tăng retries là giải pháp tạm — cần điều tra và fix gốc rễ.
--fail-on-flaky-tests (v1.45+)
Từ Playwright v1.45, flag --fail-on-flaky-tests làm cho CLI exit với non-zero code nếu bất kỳ test nào có status flaky, dù cuối cùng pass sau retry.
npx playwright test --retries=2 --fail-on-flaky-tests
Mục đích: buộc team fix flaky thay vì để nó tích lũy. Pipeline fail khi có flaky → dev phải điều tra.
Kết hợp với retries: dùng retries: 2 để biết test có thực sự bị broken hay chỉ flaky, rồi dùng --fail-on-flaky-tests để không cho phép merge khi có flaky.
Deep dive về flag này, cách tích hợp vào GitHub Actions, và quy trình xử lý flaky backlog — xem bài 77.
Pitfalls
Pitfall 1: Mỗi retry được đếm riêng trong test stats
Nếu test A chạy 3 lần (1 lần đầu + 2 retry), report đếm là 3 test attempts. Số "total tests run" trong CI output sẽ lớn hơn số test thực sự trong suite. Gây nhầm lẫn khi đọc metric CI hoặc so sánh run giữa hai ngày.
Pitfall 2: Retry không pin random seed
Nếu test tạo data ngẫu nhiên (vd: Math.random(), faker.js) mà không pin seed, mỗi retry dùng data khác nhau. Test có thể pass ở retry không phải vì flaky được giải quyết mà vì lần này random ra input dễ hơn. Flaky không reproducible, khó debug.
Pitfall 3: workerIndex không ổn định khi retry — sai khi dùng làm key
Nếu dùng workerIndex để tạo unique port, DB name, hay file path, retry sẽ spawn worker mới với workerIndex khác → resource được tạo ở index mới, không được cleanup từ lần fail trước.
// Sai: workerIndex thay đổi khi retry
const port = 3000 + testInfo.workerIndex;
// Đúng hơn: dùng parallelIndex (ổn định qua retry)
const port = 3000 + testInfo.parallelIndex;
Pitfall 4: Retry tăng CI duration đáng kể với timeout cao
Test có timeout: 60s và retries: 3 → worst case 240s per test. Suite có 50 test như vậy với 4 worker → worst case 50 × 240s / 4 = 3000s = 50 phút chỉ cho một nhóm test. Luôn tính worst case trước khi set timeout và retries cao cùng lúc.
Quiz
Câu 1. Config có retries: 2. Test fail ở lần đầu, pass ở lần retry thứ nhất. Status cuối cùng của test trong report là gì?
Đáp án
flaky. Test fail ít nhất một lần nhưng pass sau retry → Playwright đánh dấu flaky, không phải passed. Pipeline không fail (exit code 0) trừ khi dùng --fail-on-flaky-tests.
Câu 2. Suite có 50 test, timeout mỗi test là 30s, retries: 2, workers: 5. Tất cả 50 test đều flaky và fail mọi retry. Ước tính worst-case CI duration.
Đáp án
Mỗi test chạy 3 lần (1 + 2 retry) × 30s = 90s. Tổng: 50 × 90s = 4500s tổng thời gian. Chia 5 worker: 4500s / 5 = 900s = 15 phút. Đây là lý do cần ước tính worst case trước khi set retries và timeout cao.
Câu 3. Trong một retry, testInfo.retry trả về 2. Config có retries: 3. Đây là lần chạy thứ mấy và còn bao nhiêu retry nữa?
Đáp án
Lần chạy thứ 3 (attempt 0 = lần đầu, attempt 1 = retry 1, attempt 2 = retry 2 → đây là lần 3). Còn 1 retry nữa (retries: 3 nghĩa là tối đa 3 retry, đã dùng 2, còn 1). Công thức: testInfo.project.retries - testInfo.retry = 3 - 2 = 1.
Câu 4. Bạn cần dùng workerIndex để tạo unique database name cho mỗi worker. Retry sẽ ảnh hưởng gì và cách fix?
Đáp án
Retry spawn worker mới với workerIndex tăng lên → DB mới được tạo ở index mới, DB cũ từ lần fail trước không được cleanup. Cần dùng parallelIndex thay thế (ổn định qua retry vì là slot index không thay đổi), hoặc dùng worker-scope fixture với cleanup để đảm bảo DB được drop sau mỗi worker dù retry.
Câu 5. Project A config retries: 0 (override), global config có retries: 2. Dev chạy npx playwright test --project=A --retries=1. Số retry thực tế cho project A là bao nhiêu?
Đáp án
1. CLI --retries=1 có ưu tiên cao nhất và thắng cả project-level override (retries: 0) lẫn global (retries: 2). Thứ tự ưu tiên: CLI > describe > project > global > default.
Bài Tiếp Theo
Bài 75: Test Outcomes — passed, failed, flaky, skipped, interrupted — phân biệt từng trạng thái kết quả test, cách Playwright xác định outcome, ý nghĩa trong report và exit code.
