Serial mode — test.describe.serial (chạy tuần tự, phụ thuộc lẫn nhau)
automationplaywright
test.describe.serial ép các test trong nhóm chạy tuần tự đúng thứ tự trên cùng một worker, và test trước fail thì test sau bị skip. Nó dành cho flow phụ thuộc (tạo → sửa → xoá) không tách độc lập được — công cụ hữu ích nhưng là anti-pattern nếu lạm dụng thay cho test isolation.
Khái niệm & Bản chất
- Mặc định (đặc biệt với
fullyParallel: true) mỗi test độc lập và có thể bị chia sang nhiều worker chạy song song — thứ tự không đảm bảo. test.describe.serialép các test trong nhóm chạy đúng thứ tự khai báo, trên cùng 1 worker, từng test một.- Fail-fast theo chuỗi: test đầu fail → các test sau trong nhóm bị SKIP (không chạy tiếp) — tránh cascade fail rác và tiết kiệm thời gian.
- Serial chỉ ràng buộc trong nhóm — các nhóm/test khác ngoài describe đó vẫn song song bình thường.
- Serial không đồng nghĩa cùng browser context — mỗi test vẫn nhận
page/contextmới; chỉ share state qua biến JS (cùng worker process) hoặc worker-scoped fixture.
Cách hoạt động & API chính
- Khai báo:
test.describe.serial('flow', () => { ... })— hoặctest.describe.configure({ mode: 'serial' })trong describe. - Truyền state giữa test qua biến closure trong describe (vd
let orderIdgán ở test 1, đọc ở test 2) — khả thi vì cùng worker. beforeAll/afterAlltrong nhóm serial chạy 1 lần quanh cả chuỗi.- Kết hợp được:
test.describe.serial.only,.skip,.fixme; và annotation điều kiện bên trong vẫn hoạt động. - Với retry: khi một test trong chuỗi serial fail và được retry, Playwright chạy lại toàn bộ nhóm serial từ đầu — vì test phụ thuộc cần chuỗi state.
test.describe.parallellà chế độ ngược lại — test trong nhóm chạy song song với nhau (hiếm dùng, cần tự lo race condition).
Ví dụ & Cú pháp thực tế
- Vòng đời đơn hàng — test sau dùng kết quả test trước:
``
ts import { test, expect } from '@playwright/test'; test.describe.serial('vòng đời đơn hàng', () => { let orderId: string; // state share giữa các test trong chuỗi test('tạo đơn mới', async ({ page }) => { await page.goto('/products'); await page.getByRole('button', { name: 'Mua ngay' }).first().click(); await page.getByRole('button', { name: 'Đặt hàng' }).click(); // lưu mã đơn cho các test sau orderId = await page.locator('.order-id').innerText(); expect(orderId).toMatch(/^ORD-/); }); test('thanh toán đơn', async ({ page }) => { await page.goto(/orders/${orderId}); // dùng orderId từ test trước await page.getByRole('button', { name: 'Thanh toán' }).click(); await expect(page.getByText('Đã thanh toán')).toBeVisible(); }); test('huỷ đơn', async ({ page }) => { // nếu test 'thanh toán đơn' fail → test này tự SKIP await page.goto(/orders/${orderId}); await page.getByRole('button', { name: 'Huỷ đơn' }).click(); await expect(page.getByText('Đã huỷ')).toBeVisible(); }); });`` - Serial có điều kiện theo env — chỉ chạy chuỗi này trên staging:
``
ts test.describe.serial('nightly data migration flow', () => { test.skip(process.env.ENV !== 'staging', 'Chỉ chạy trên staging'); // ... });`` - Setup 1 lần cho cả chuỗi bằng beforeAll trong nhóm serial:
``
ts test.describe.serial('quản lý kho', () => { test.beforeAll(async ({ request }) => { // seed sản phẩm 1 lần cho cả chuỗi qua API await request.post('/api/seed/product', { data: { sku: 'SERIAL-01' } }); }); // ... });``
Best practices & Khi nào dùng
- Dùng khi: flow dài không tách được (đăng ký → verify email → đổi mật khẩu), setup rất đắt mà tái tạo mỗi test quá chậm, hoặc app stateful chỉ cho 1 phiên.
- Không dùng khi có thể tách độc lập: tạo data bằng API/fixture trước mỗi test → chạy parallel, nhanh và ổn định hơn hẳn. Serial là phương án B, không phải default.
- Giữ chuỗi ngắn 2–4 test — chuỗi dài khó debug, fail giữa đường mất nửa suite và retry tốn kém (rerun cả chuỗi).
- Mỗi test trong chuỗi vẫn phải assert đầy đủ — đừng viết test chỉ để 'chuẩn bị' cho test sau; phần chuẩn bị thuộc về
beforeAllhoặc fixture. - Nếu các bước phụ thuộc 100% và chỉ quan tâm kết quả cuối — cân nhắc gom về 1 test + nhiều
test.step: report vẫn đẹp, không tốn overhead nhiều test và tránh cạm bẫy share state.
Gotchas, So sánh & Lưu ý
- Serial ≠ cùng page/context — mỗi test vẫn context mới tinh, localStorage/cookie không mang qua; share state chỉ qua biến JS/worker fixture/storageState.
- Biến closure là hidden dependency — người đọc không biết
orderIdtừ đâu ra; comment rõ test nào gán, test nào dùng. fullyParallelbận vẫn tôn trọng serial — nhưng nhóm serial chạy song song với các test khác → race condition nếu đụng chung tài nguyên (cùng tài khoản, cùng SKU seed). Dùng data riêng cho nhóm serial.- Retry rerun cả chuỗi — 1 test flaky ở cuối kéo toàn bộ chạy lại; cân nhắc trước khi bật retries cao với serial.
- Test đầu fail → skip hàng loạt → coverage giảm âm thầm; theo dõi số skipped trên report, đừng chỉ nhìn số xanh.
- So sánh 1 test nhiều step: serial cho phép từng bước có fixture/teardown riêng và đếm riêng trên report — nhưng 1 test + step đơn giản và ít lỗi hơn khi flow thực sự là một giao dịch.
