Danh sách bài viết

Bài 65: Mode 'serial' | 'default' | 'parallel' — Tổng Quan Từ Góc Nhìn Parallel

Playwright có 3 giá trị mode cho test.describe.configure(): 'serial', 'default', và 'parallel'. Bài này không lặp lại detail của từng mode — các bài 36, 37, 38 đã cover. Thay vào đó, bài tập trung vào ranh giới giữa 3 mode qua 3 cấp cấu hình (project, file, describe), bảng so sánh tổng hợp, decision tree để chọn mode phù hợp, pattern hybrid khi migrate project cũ, và throughput impact khi mix các cấp.

28/05/2026
0 lượt xem
1

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: true kết hợp file/describe serial override.
  • Hiểu throughput impact của từng mode và cách workers tương tác với mode.
  • Nắm migration matrix khi chuyển project từ serial sang parallel.
2

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ự:

  1. Explicit configure() trong describe gần nhất chứa test.
  2. Nếu describe đó không có configure → leo lên parent describe.
  3. Nếu không có parent describe → file-level configure.
  4. 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

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/afterAll chạ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ừ project fullyParallel. Kết quả là parallel nếu fullyParallel: true, serial nếu fullyParallel: false.

Cross-reference: bài 36 — serial deep dive; bài 37 — parallel deep dive; bài 38 — default deep dive; bài 64fullyParallel config.

4

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)
5

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)
6

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 đó.

7

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.

8

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.

9

--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' }

--workersworkers config xác định dung lượng của worker pool — bao nhiêu "làn đường" song song. modefullyParallel 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
10

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

  1. Bật fullyParallel: true trong config.
  2. Chạy toàn suite — ghi lại các test/file fail do race condition hoặc state conflict.
  3. Với file fail: thêm test.describe.configure({ mode: 'serial' }) ở đầu file → file đó trở về serial, test không fail nữa.
  4. 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
11

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 }) => { /* ... */ });
});
12

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.