Mục lục
Mục Tiêu Bài Học
Bài 36–38 đã phân tích từng mode riêng lẻ. Bài này nhìn từ góc độ tổng thể:
- Hiểu 3 cấp cấu hình mode và thứ tự ưu tiên giữa chúng.
- Nắm ranh giới hành vi giữa
'serial','default','parallel'khi chúng tương tác. - Có decision tree để chọn mode phù hợp cho từng tình huống.
- Biết pattern hybrid: project
fullyParallel: truekết hợp file/describe serial override. - Hiểu throughput impact của từng mode và cách
workerstương tác với mode. - Nắm migration matrix khi chuyển project từ serial sang parallel.
3 Cấp Đặt Mode
Mode của một test được xác định từ 3 cấp, thứ tự ưu tiên từ cao đến thấp:
Cấp 1 — Project level
Cấu hình trong playwright.config.ts qua fullyParallel:
// playwright.config.ts
export default defineConfig({
fullyParallel: true, // hoặc false (mặc định là false nếu không khai báo)
workers: 4,
});
fullyParallel: true — mọi test trong mọi file chạy parallel trừ khi có override cấp dưới. fullyParallel: false (hoặc không khai báo) — test trong cùng file chạy serial, các file có thể chạy song song với nhau.
Cấp 2 — File level
Khai báo test.describe.configure() ở đầu file, ngoài mọi describe block:
// tests/legacy.spec.ts
import { test, expect } from '@playwright/test';
// File-level: áp dụng cho toàn bộ file này
test.describe.configure({ mode: 'serial' });
test('step 1', async ({ page }) => { /* ... */ });
test('step 2', async ({ page }) => { /* ... */ });
File-level configure override project config cho tất cả test và describe trong file đó. File khác trong cùng project không bị ảnh hưởng.
Cấp 3 — Describe level
Khai báo test.describe.configure() bên trong describe callback, là dòng đầu tiên:
// Áp dụng chỉ cho describe block này
test.describe('Isolated section', () => {
test.describe.configure({ mode: 'parallel' });
test('case A', async ({ page }) => { /* ... */ });
test('case B', async ({ page }) => { /* ... */ });
});
Describe-level configure override cả file-level và project config, nhưng chỉ trong phạm vi describe đó. Describe anh em và describe cha không bị ảnh hưởng.
Hierarchy tóm tắt
Khi Playwright xác định mode của một test, nó tìm theo thứ tự:
- Explicit
configure()trong describe gần nhất chứa test. - Nếu describe đó không có configure → leo lên parent describe.
- Nếu không có parent describe → file-level configure.
- Nếu không có file-level configure → project
fullyParallel.
Ngoại lệ: mode: 'default' skip bước 2 và 3 — nhảy thẳng về project config. Chi tiết đã có tại bài 38.
3 Giá Trị Mode — Nhắc Nhanh
Bài 36, 37, 38 đã phân tích chi tiết. Đây chỉ là nhắc lại để tham chiếu trong bảng so sánh ở phần tiếp theo:
'parallel': test trong scope chạy song song — mỗi test được Playwright giao cho worker khả dụng. Thứ tự thực thi không xác định.beforeAll/afterAllchạy per-worker.'serial': test trong scope chạy tuần tự theo thứ tự khai báo trên 1 worker duy nhất. Test fail → các test sau bịexpected-skipped.'default': không ép mode cụ thể — bypass hierarchy kế thừa, đọc thẳng từ projectfullyParallel. Kết quả là parallel nếufullyParallel: true, serial nếufullyParallel: false.
Cross-reference: bài 36 — serial deep dive; bài 37 — parallel deep dive; bài 38 — default deep dive; bài 64 — fullyParallel config.
Bảng So Sánh Tổng Hợp
Bảng dưới tổng hợp 4 cấu hình phổ biến nhất, kết hợp project config và file/describe mode:
| Scenario | fullyParallel | Mode (file/describe) | Behavior thực tế |
|---|---|---|---|
| Project default — parallel tối đa | true |
Không khai báo (inherited default) |
Mọi test trong mọi file chạy parallel, phân phối tự do qua worker pool |
| Project conservative — serial mặc định | false |
Không khai báo (inherited default) |
Test trong cùng file chạy serial; các file khác nhau có thể chạy song song trên worker riêng |
| File override — ép serial | bất kỳ | 'serial' (file-level) |
File đó chạy trên 1 worker, test tuần tự; test fail → cascade skip; file khác không bị ảnh hưởng |
| Describe parallel trong file serial | bất kỳ | 'parallel' (describe-level) |
Describe đó chạy parallel; các describe khác trong file vẫn theo mode của chúng |
Kết hợp các cấp cụ thể hơn:
| fullyParallel | File-level mode | Describe-level mode | Kết quả cho describe đó |
|---|---|---|---|
true |
— | — | Parallel (theo project) |
true |
— | 'serial' |
Serial (describe override) |
true |
'serial' |
— | Serial (kế thừa file-level) |
true |
'serial' |
'parallel' |
Parallel (describe override file-level) |
true |
'serial' |
'default' |
Parallel (default → project fullyParallel: true) |
false |
— | — | Serial (theo project) |
false |
— | 'parallel' |
Parallel (describe override) |
false |
'serial' |
'default' |
Serial (default → project fullyParallel: false) |
Decision Tree — Chọn Mode Nào?
Các câu hỏi để chọn cấu hình mode:
Câu hỏi 1: Test có hoàn toàn isolated không?
Mỗi test tự setup/teardown state riêng, không chia sẻ gì với test khác ngoài fixture tiêu chuẩn? Không có phụ thuộc thứ tự?
→ Có: Dùng fullyParallel: true ở project level. Đây là cấu hình throughput cao nhất và khuyến nghị mặc định của Playwright.
→ Không: Tiếp tục câu hỏi 2.
Câu hỏi 2: State coupling có ở toàn file hay chỉ trong một nhóm test?
→ Toàn file (mọi test trong file phụ thuộc nhau theo thứ tự): Dùng test.describe.configure({ mode: 'serial' }) ở file-level. Các file khác vẫn parallel nếu project có fullyParallel: true.
→ Chỉ một nhóm: Dùng test.describe.configure({ mode: 'serial' }) ở describe-level cho nhóm đó. Các describe khác trong file tự do theo mode của chúng.
Câu hỏi 3: Cần override parallel cho describe trong project/file serial?
Project hoặc file đang serial, nhưng một nhóm test đã verified là independent và muốn tăng tốc?
→ Dùng mode: 'parallel' ở describe-level để opt-in parallel tường minh, bất kể project config.
Câu hỏi 4: Describe nằm trong parent serial, muốn theo project config thay vì kế thừa?
→ Dùng mode: 'default' để bypass kế thừa từ parent và đọc trực tiếp từ fullyParallel. Kết quả phụ thuộc project config.
Tóm tắt decision tree
Test isolated hoàn toàn?
├── Có → fullyParallel: true (project)
└── Không
├── State coupling toàn file → mode: 'serial' (file-level)
├── State coupling chỉ 1 describe → mode: 'serial' (describe-level)
├── Override parallel cho 1 describe → mode: 'parallel' (describe-level)
└── Bypass kế thừa parent, theo project → mode: 'default' (describe-level)
Pattern Hybrid: fullyParallel + Serial Override
Pattern phổ biến khi project lớn: project bật fullyParallel: true nhưng vẫn có một số file hoặc describe cần serial vì lý do kỹ thuật không tránh được.
// playwright.config.ts — parallel mặc định cho toàn project
export default defineConfig({
fullyParallel: true,
workers: 4,
use: {
baseURL: 'http://localhost:3000',
},
});
// tests/legacy.spec.ts — file cũ chưa migrate, ép serial toàn file
import { test, expect } from '@playwright/test';
test.describe.configure({ mode: 'serial' });
test.describe('Legacy suite', () => {
test('step 1', async ({ page }) => {
await page.goto('/legacy/setup');
await page.getByRole('button', { name: 'Initialize' }).click();
await expect(page.getByText('Ready')).toBeVisible();
});
test('step 2', async ({ page }) => {
// Phụ thuộc state từ step 1 — legacy code, chưa refactor
await page.goto('/legacy/run');
await expect(page.getByText('Running')).toBeVisible();
});
});
// tests/new.spec.ts — file mới, test isolated, parallel theo project
import { test, expect } from '@playwright/test';
test('case A', async ({ page }) => {
await page.goto('/products');
await expect(page.getByRole('list')).toBeVisible();
});
test('case B', async ({ page }) => {
await page.goto('/cart');
await expect(page.getByRole('heading')).toHaveText('Shopping Cart');
});
Trong cấu hình này: new.spec.ts chạy parallel theo fullyParallel: true. legacy.spec.ts chạy serial trên 1 worker do file-level configure. Khi chạy cùng nhau, hai file hoạt động trên worker pool riêng biệt — new.spec.ts tận dụng đa nhân, legacy.spec.ts chiếm 1 worker.
Variant: serial describe trong file parallel
// tests/mixed.spec.ts — file này không có file-level configure
// Project: fullyParallel: true → mọi describe mặc định parallel
test.describe('Checkout flow', () => {
// Override: cần serial vì test share session state
test.describe.configure({ mode: 'serial' });
test('add item to cart', async ({ page }) => {
await page.goto('/shop');
await page.getByRole('button', { name: 'Add to cart' }).first().click();
});
test('proceed to checkout', async ({ page }) => {
// Trong thực tế, pattern này cần shared page qua beforeAll
// Xem bài 36 cho full implementation
await page.goto('/cart');
await page.getByRole('link', { name: 'Checkout' }).click();
});
});
test.describe('Product catalog', () => {
// Không configure → theo fullyParallel: true → parallel
test('filter by price', async ({ page }) => {
await page.goto('/products?sort=price');
await expect(page.getByRole('listitem').first()).toBeVisible();
});
test('filter by category', async ({ page }) => {
await page.goto('/products?category=electronics');
await expect(page.getByRole('listitem').first()).toBeVisible();
});
});
Trong file này: "Checkout flow" chạy serial (describe override); "Product catalog" chạy parallel (theo project). Hai describe không chạy đồng thời — Playwright xử lý chúng tuần tự trong file, nhưng test bên trong mỗi describe theo mode của describe đó.
Mode Và Throughput
Mode ảnh hưởng trực tiếp đến throughput — số test hoàn thành trên một đơn vị thời gian:
parallel — throughput cao nhất
Test được phân phối tự do qua worker pool. Với workers: N, tối đa N test chạy đồng thời. Thời gian tổng ≈ thời gian test dài nhất trong nhóm (nếu đủ worker).
5 test, mỗi test 2s, workers: 5
→ Tất cả chạy cùng lúc → tổng ≈ 2s
5 test, mỗi test 2s, workers: 2
→ 2 + 2 + 1 test theo batch → tổng ≈ 6s
serial — throughput thấp nhất trong scope
Toàn bộ scope chạy trên 1 worker, test nối đuôi nhau. Thời gian tổng = tổng thời gian tất cả test trong scope. Các scope khác (file khác, describe khác) vẫn chạy song song với scope serial này.
5 test serial, mỗi test 2s → tổng scope = 10s
Trong khi đó: file khác parallel vẫn chạy đồng thời trên worker riêng
default — phụ thuộc project
Throughput của 'default' bằng throughput của 'parallel' khi fullyParallel: true, và bằng throughput của 'serial' khi fullyParallel: false.
Ví dụ so sánh
Cùng 20 test, workers: 4, mỗi test 3 giây:
| Cấu hình | Thời gian ước tính | Ghi chú |
|---|---|---|
| fullyParallel: true (tất cả parallel) | ≈ 15s | 20 test / 4 worker = 5 batch × 3s |
| fullyParallel: false (tất cả serial) | ≈ 60s | 20 test × 3s, 1 worker tại 1 thời điểm trong file |
| Hybrid: 15 parallel + 5 serial | ≈ 15–27s | Phụ thuộc worker allocation và thứ tự scheduling |
Lưu ý: đây là ước tính đơn giản, bỏ qua overhead spawn worker, time scheduling, và network latency. Thực tế phức tạp hơn — đo benchmark thực tế trên project cụ thể mới có số chính xác.
Mode Tương Tác Với Workers Như Thế Nào
Hai khái niệm cần phân biệt rõ: workers là số process chạy đồng thời; mode là cách test được phân phối cho các process đó. Chúng độc lập nhau.
serial + workers: 4
Describe có mode: 'serial' chạy trên 1 worker duy nhất — 3 worker còn lại rảnh (hoặc nhận test từ file/describe khác). Test trong describe serial không bao giờ split qua nhiều worker.
workers: 4, file A (serial) 6 test, file B (parallel) 8 test
Worker 1: File A — test 1, 2, 3, 4, 5, 6 (tuần tự)
Worker 2: File B — test 1, 4, 7 (parallel)
Worker 3: File B — test 2, 5, 8 (parallel)
Worker 4: File B — test 3, 6 (parallel)
parallel + workers: 4
Test trong describe parallel được split qua tối đa 4 worker. Nếu describe có 10 test và workers là 4, Playwright phân phối 10 test vào queue và giao cho worker nào rảnh trước.
workers: 4, describe có mode: 'parallel', 10 test
Worker 1: test 1, test 5, test 9
Worker 2: test 2, test 6, test 10
Worker 3: test 3, test 7
Worker 4: test 4, test 8
(thứ tự thực tế phụ thuộc completion time của từng test)
Giới hạn worker không thay đổi mode
Tăng workers không tự động biến serial thành parallel. Giảm workers: 1 không biến parallel thành serial về mặt intent — test vẫn có mode: 'parallel' nhưng chạy tuần tự vì chỉ có 1 worker. Behavior skip-on-fail của serial vẫn hoạt động bất kể số worker là bao nhiêu.
--workers CLI vs mode
Hai thứ hay bị nhầm lẫn khi cấu hình parallelism trong Playwright:
| Tham số | Kiểm soát cái gì | Ví dụ |
|---|---|---|
--workers N (CLI) |
Số worker process tối đa chạy đồng thời | npx playwright test --workers 2 |
workers: N (config) |
Như trên, nhưng khai báo trong config | workers: process.env.CI ? 2 : 4 |
fullyParallel (config) |
Test trong cùng file có được split qua nhiều worker không | fullyParallel: true |
mode (describe.configure) |
Behavior phân phối test trong scope đó | { mode: 'serial' } |
--workers và workers config xác định dung lượng của worker pool — bao nhiêu "làn đường" song song. mode và fullyParallel xác định cách test được điều phối vào các làn đường đó.
Ví dụ: --workers 1 với fullyParallel: true — mọi test được queue theo kiểu parallel nhưng chỉ có 1 worker xử lý, nên thực tế chạy tuần tự. Behavior skip-on-fail của mode: 'serial' vẫn hoạt động đúng ngay cả khi --workers 1.
# Chạy với 1 worker — CI tiết kiệm tài nguyên
npx playwright test --workers 1
# Chạy với số worker bằng CPU cores
npx playwright test --workers $(nproc)
# Override config workers tạm thời
npx playwright test --workers 2
Migration Matrix
Tình huống phổ biến: project cũ dùng fullyParallel: false (hoặc không set), muốn migrate sang fullyParallel: true để tăng tốc CI. Không thể đổi tất cả cùng lúc vì một số test có state coupling.
Quy trình migration từng bước
- Bật
fullyParallel: truetrong config. - Chạy toàn suite — ghi lại các test/file fail do race condition hoặc state conflict.
- Với file fail: thêm
test.describe.configure({ mode: 'serial' })ở đầu file → file đó trở về serial, test không fail nữa. - Refactor dần: tách state coupling, làm test isolated — sau đó xóa
mode: 'serial'để file được hưởng lợi parallel.
// playwright.config.ts — bước 1: bật fullyParallel
export default defineConfig({
fullyParallel: true, // Đổi từ false hoặc không có
workers: process.env.CI ? 2 : 4,
});
// tests/checkout.spec.ts — bước 3: mark serial để ổn định
import { test, expect } from '@playwright/test';
// TODO: refactor để xóa dòng này — ticket #1234
test.describe.configure({ mode: 'serial' });
test('add to cart', async ({ page }) => { /* ... */ });
test('checkout', async ({ page }) => { /* ... */ });
test('confirm order', async ({ page }) => { /* ... */ });
// tests/catalog.spec.ts — đã verify stable → không cần configure
// fullyParallel: true → parallel theo mặc định
import { test, expect } from '@playwright/test';
test('list products', async ({ page }) => { /* ... */ });
test('search products', async ({ page }) => { /* ... */ });
test('filter products', async ({ page }) => { /* ... */ });
Tracking progress migration
Cách đơn giản để track: đếm số file có mode: 'serial' top-level. Mục tiêu migration là đưa con số này về 0 — tất cả test isolated, không cần override.
# Đếm số file còn serial override — chạy từ root project
grep -rl "describe.configure.*serial" tests/ | wc -l
Pitfall
1. Quên khai báo fullyParallel: true — mọi file serial despite intent
Playwright không tự bật fullyParallel: true sau khi cài đặt. Nếu không khai báo, giá trị mặc định là false. Nhiều dev nghĩ rằng cài workers: 4 là đủ để chạy parallel — nhưng với fullyParallel: false, test trong cùng file vẫn serial.
// SAI — workers: 4 nhưng test vẫn serial trong từng file
export default defineConfig({
workers: 4,
// fullyParallel không khai báo → mặc định false
});
// ĐÚNG — cần cả hai
export default defineConfig({
fullyParallel: true,
workers: 4,
});
2. File mode: 'serial' + test viết isolated — lãng phí parallelism
File được đánh dấu serial nhưng test bên trong hoàn toàn independent. Mỗi test tự setup/teardown, không share state. Kết quả: chạy serial không cần thiết, throughput thấp hơn.
// SAI — test isolated nhưng file đang serial
test.describe.configure({ mode: 'serial' }); // không cần thiết
test('check homepage', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle('Home');
});
test('check about page', async ({ page }) => {
await page.goto('/about');
await expect(page).toHaveTitle('About');
});
Dấu hiệu nhận ra: nếu xóa mode: 'serial' và test vẫn pass ổn, file không cần serial.
3. Nested describe override mode — hành vi không mong đợi
Describe con khai báo mode khác với parent — kết quả có thể bất ngờ nếu không nắm rõ hierarchy.
// playwright.config.ts: fullyParallel: true
test.describe('Parent', () => {
test.describe.configure({ mode: 'serial' });
test('parent test 1', async ({ page }) => { /* serial */ });
test('parent test 2', async ({ page }) => { /* serial, sau parent test 1 */ });
test.describe('Child', () => {
// Không configure → kế thừa serial từ parent
// Dev nghĩ: "không configure → theo project (parallel)"
// Thực tế: inherit serial từ parent
test('child test 1', async ({ page }) => { /* serial! */ });
test('child test 2', async ({ page }) => { /* serial! */ });
});
});
Để child chạy parallel khi parent là serial: cần khai báo rõ mode: 'parallel' hoặc mode: 'default' (nếu muốn theo project).
4. Mix mode + fullyParallel không nhất quán — confusing
Codebase có fullyParallel: true nhưng nhiều file/describe có mode: 'serial' mà không có comment giải thích. Dev mới đọc code không biết serial là có lý do kỹ thuật hay chỉ là legacy chưa xóa.
// Thiếu context — tại sao serial?
test.describe.configure({ mode: 'serial' });
test('login', async ({ page }) => { /* ... */ });
test('dashboard', async ({ page }) => { /* ... */ });
// Rõ ràng hơn — có comment giải thích
// Serial: 2 test share browser session qua biến closure + beforeAll
// TODO: refactor sang isolated khi có thời gian — ticket #567
test.describe.configure({ mode: 'serial' });
5. Nhầm mode: 'default' với mode: 'parallel'
Khi project có fullyParallel: false, khai báo mode: 'default' không cho kết quả parallel — nó reset về project config, tức là serial. Nếu mục đích là parallel bất kể project, phải khai báo mode: 'parallel' explicit.
// playwright.config.ts: fullyParallel: false
// SAI nếu mục đích là parallel
test.describe('Read tests', () => {
test.describe.configure({ mode: 'default' });
// 'default' = serial (theo fullyParallel: false) — không phải parallel!
test('read A', async ({ page }) => { /* ... */ });
test('read B', async ({ page }) => { /* ... */ });
});
// ĐÚNG nếu mục đích là parallel bất kể project config
test.describe('Read tests', () => {
test.describe.configure({ mode: 'parallel' });
test('read A', async ({ page }) => { /* ... */ });
test('read B', async ({ page }) => { /* ... */ });
});
Quiz
Câu 1. Project có fullyParallel: true, workers: 4. File A có test.describe.configure({ mode: 'serial' }) top-level với 5 test. File B không có configure, 8 test. Khi chạy cả hai file, bao nhiêu worker được file A sử dụng?
Đáp án
1 worker — file A có file-level mode: 'serial', toàn bộ 5 test chạy tuần tự trên 1 worker. 3 worker còn lại sẵn sàng nhận test từ file B (parallel theo fullyParallel: true). File A và file B chạy đồng thời: file A chiếm 1 worker serial, file B phân phối qua tối đa 3 worker còn lại.
Câu 2. Trong migration từ fullyParallel: false sang true, file test có 3 describe: describe X (state coupling thật sự), describe Y (đã verify isolated), describe Z (chưa review). Cần làm gì với mỗi describe?
Đáp án
Describe X: thêm test.describe.configure({ mode: 'serial' }) để opt-out parallel — state coupling cần serial. Describe Y: có thể để mặc định (hưởng parallel từ fullyParallel: true) hoặc thêm mode: 'default' để ghi lại intent "đã verify". Describe Z: để mặc định và quan sát khi chạy — nếu fail, thêm mode: 'serial' tạm thời; nếu pass, không cần làm gì thêm.
Câu 3. Describe A (parent) có mode: 'serial'. Describe B (nested trong A) không có configure. Describe C (nested trong A) có mode: 'default'. Project có fullyParallel: true. Mode thực tế của B và C là gì?
Đáp án
Describe B: serial — không có configure, kế thừa từ parent A (serial). Describe C: parallel — mode: 'default' bypass kế thừa từ parent, đọc thẳng từ project config (fullyParallel: true), kết quả là parallel. B và C có mode khác nhau dù cùng nằm trong parent A.
Câu 4. Dev chạy npx playwright test --workers 1 trên project có fullyParallel: true. Describe có mode: 'serial' — skip-on-fail có hoạt động không?
Đáp án
Có — skip-on-fail (cascade expected-skipped) là behavior của mode: 'serial', không phụ thuộc số worker. Dù --workers 1, nếu test fail trong serial describe, các test sau vẫn bị expected-skipped. --workers chỉ kiểm soát dung lượng pool, không thay đổi mode behavior.
Câu 5. File có 2 describe không configure gì cả. Project fullyParallel: false. Dev đổi config sang fullyParallel: true. Hai describe đó thay đổi behavior thế nào — và có nguy cơ gì?
Đáp án
Cả hai describe chuyển từ serial sang parallel (vì không có configure explicit, chúng theo project config). Nguy cơ: nếu test bên trong có state coupling ẩn (biến closure, shared resource, thứ tự phụ thuộc) mà trước đây serial che khuất, chúng có thể fail do race condition hoặc state conflict. Cách phát hiện: chạy suite sau khi đổi config và quan sát test fail mới xuất hiện.
