Danh sách bài viết

Bài 64: fullyParallel: true — Parallelize Cả Trong File

Mặc định Playwright chạy song song giữa các file nhưng chạy tuần tự bên trong mỗi file. fullyParallel: true phá vỡ giới hạn đó: mỗi test trong cùng file được phân bổ sang worker riêng, cho phép suite lớn tận dụng tối đa worker pool. Bài này phân tích cơ chế, trade-off isolation, behavior của beforeAll, cách kết hợp với describe.configure({ mode: 'serial' }), pattern migration từ legacy code, và các pitfall thường gặp.

28/05/2026
13 phút đọc
0 lượt xem
1

Mục Tiêu Bài Học

Sau khi hoàn thành bài này, bạn sẽ:

  • Hiểu sự khác biệt giữa fullyParallel: falsefullyParallel: true ở mức worker assignment.
  • Biết tại sao fullyParallel: true yêu cầu test phải isolated.
  • Nắm rõ behavior của beforeAll khi bật fullyParallel.
  • Biết cách kết hợp với describe.configure({ mode: 'serial' }) cho describe có state phụ thuộc.
  • Áp dụng được migration pattern từ legacy suite sang fullyParallel: true.
2

fullyParallel Là Gì?

fullyParallel là config option trong playwright.config.ts, kiểm soát đơn vị phân phối lên worker:

  • false (mặc định trong Playwright v1.x cũ): đơn vị phân phối là file. Các file chạy song song, nhưng test bên trong một file chạy tuần tự trên cùng 1 worker.
  • true (mặc định trong các bản v1.x gần đây): đơn vị phân phối là test. Mỗi test có thể được giao cho bất kỳ worker nào đang rảnh.

Cú pháp khai báo:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  fullyParallel: true,
  workers: 4,
});

Lưu ý về default: Từ Playwright v1.32+, npx init playwright@latest tạo config với fullyParallel: true sẵn. Nếu bạn đang dùng config cũ không khai báo option này, hành vi thực tế phụ thuộc vào version Playwright đang cài — kiểm tra package.json để xác nhận.

3

Behavior false vs true

fullyParallel: false

Giả sử có 2 file, mỗi file 3 test, chạy với workers: 2:

// file-a.spec.ts: testA1, testA2, testA3
// file-b.spec.ts: testB1, testB2, testB3

Worker 1: [testA1 → testA2 → testA3]  (file-a.spec.ts chạy serial)
Worker 2: [testB1 → testB2 → testB3]  (file-b.spec.ts chạy serial)

Các file được phân bổ song song, nhưng test trong mỗi file vẫn chạy theo thứ tự trên 1 worker duy nhất. Nếu một file có nhiều test hơn file kia, worker của file đó sẽ bận lâu hơn — worker kia phải chờ file tiếp theo.

fullyParallel: true

Cùng ví dụ trên với workers: 2:

// Playwright phân bổ test không theo thứ tự file

Worker 1: [testA1] → [testA3] → [testB2]
Worker 2: [testA2] → [testB1] → [testB3]

Không có đảm bảo về thứ tự. Playwright load-balance test vào worker available. Nếu workers: 4 và có 6 test, 4 test đầu chạy ngay, 2 test còn lại được giao khi worker rảnh.

So sánh tóm tắt

Tiêu chí false true
Đơn vị phân phối File Test
Test trong file Serial, cùng worker Song song, nhiều worker
Tốc độ (suite lớn) Phụ thuộc kích thước file Maximum throughput
Yêu cầu isolation File-level OK Test-level bắt buộc
beforeAll chạy 1 lần / file 1 lần / worker dùng file đó
4

Worker Assignment

Khi fullyParallel: true, Playwright duy trì một worker pool theo workers config. Mỗi test được dequeue và giao cho worker đang idle đầu tiên — không có ưu tiên theo file hay thứ tự khai báo.

Điều này có nghĩa:

  • 2 test trong cùng 1 file có thể chạy đồng thời trên 2 worker khác nhau.
  • Không có cơ chế "test này phải chạy trước test kia trong cùng file" — trừ khi dùng describe.configure({ mode: 'serial' }) hoặc test.step.
  • Thứ tự test trong HTML report timeline có thể khác hoàn toàn thứ tự khai báo trong file.

Để test hoạt động đúng với fullyParallel: true, mỗi test phải fresh state:

// Đúng: mỗi test dùng beforeEach để reset
test.describe('User profile', () => {
  let userId: string;

  test.beforeEach(async ({ request }) => {
    // Tạo user mới cho mỗi test — không dùng chung
    const res = await request.post('/api/users', {
      data: { name: 'Test User' }
    });
    userId = (await res.json()).id;
  });

  test.afterEach(async ({ request }) => {
    await request.delete(`/api/users/${userId}`);
  });

  test('view profile', async ({ page }) => {
    await page.goto(`/users/${userId}`);
    // ...
  });

  test('edit profile', async ({ page }) => {
    await page.goto(`/users/${userId}/edit`);
    // ...
  });
});

Khi 2 test trên chạy đồng thời, mỗi test có userId riêng — không conflict.

5

beforeAll Overhead

Đây là điểm cần chú ý nhất khi bật fullyParallel: true.

Với fullyParallel: false, 1 file chạy trên 1 worker — beforeAll chạy đúng 1 lần cho toàn bộ test trong file. Với fullyParallel: true, các worker khác nhau nhận test từ cùng 1 file — mỗi worker cần tự thực thi beforeAll của file đó.

Ví dụ minh họa:

// auth.spec.ts — 10 test, workers: 4
test.beforeAll(async () => {
  // Setup: seed database, khởi động server mock
  // Mất ~2 giây
  await seedDatabase();
});

test('login flow', async ({ page }) => { /* ... */ });
test('logout flow', async ({ page }) => { /* ... */ });
// ... 8 test khác

Khi fullyParallel: true với 4 worker, Playwright phân bổ 10 test lên 4 worker. Mỗi worker nhận ít nhất 1 test từ file này → beforeAll chạy 4 lần thay vì 1 lần.

Hệ quả thực tế:

  • Thời gian setup tăng nếu beforeAll nặng.
  • Port conflict: nếu beforeAll khởi động HTTP server trên port cố định, 2 worker chạy đồng thời → conflict ngay.
  • DB seed duplicate: nếu beforeAll insert row với unique constraint, 2 worker cùng insert → lỗi constraint.

Cách xử lý:

// Dùng port động thay vì cố định
test.beforeAll(async () => {
  // Không hardcode port
  server = await startServer({ port: 0 }); // port: 0 = OS assign
  baseURL = `http://localhost:${server.address().port}`;
});
// Dùng unique prefix per worker
import { test } from '@playwright/test';

test.beforeAll(async ({}, testInfo) => {
  const workerId = testInfo.parallelIndex;
  await seedDatabase({ prefix: `worker_${workerId}_` });
});
6

Kết Hợp describe.configure({ mode: 'serial' })

fullyParallel: true hoạt động ở project/global level. describe.configure({ mode: 'serial' }) hoạt động ở describe level và override fullyParallel cho describe đó.

Combine pattern: bật fullyParallel: true toàn project, nhưng mark riêng các describe cần tuần tự:

// playwright.config.ts
export default defineConfig({
  fullyParallel: true,
  workers: 4,
});
// checkout.spec.ts
import { test } from '@playwright/test';

// Describe này cần chạy tuần tự vì share cart state
test.describe('Checkout flow', () => {
  test.describe.configure({ mode: 'serial' });

  test('add item to cart', async ({ page }) => { /* ... */ });
  test('proceed to payment', async ({ page }) => { /* ... */ });
  test('confirm order', async ({ page }) => { /* ... */ });
});

// Describe này không có phụ thuộc — chạy parallel bình thường
test.describe('Product listing', () => {
  test('filter by category', async ({ page }) => { /* ... */ });
  test('sort by price', async ({ page }) => { /* ... */ });
  test('search by keyword', async ({ page }) => { /* ... */ });
});

Kết quả: 3 test trong "Product listing" chạy song song với nhau và có thể song song với describe "Checkout flow". Trong "Checkout flow", 3 test chạy tuần tự theo thứ tự khai báo trên 1 worker.

Lưu ý: Chi tiết về describe.configure({ mode: 'serial' }) — bao gồm skip-on-fail, share state qua biến, retry behavior — đã được trình bày ở bài 36.

7

Override Theo Project

Khi suite có nhiều project với yêu cầu khác nhau, có thể override fullyParallel per project:

export default defineConfig({
  // Global default: parallel
  fullyParallel: true,
  workers: 4,

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
      // Kế thừa fullyParallel: true từ global
    },
    {
      name: 'e2e-serial',
      testDir: './tests/e2e-serial',
      fullyParallel: false, // Override: file-level parallel, test serial trong file
    },
    {
      name: 'unit',
      testDir: './tests/unit',
      fullyParallel: true,  // Explicit: test-level parallel
    },
  ],
});

Project e2e-serial chứa test legacy có shared state trong file — bật fullyParallel: false để giữ behavior cũ trong khi vẫn refactor dần. Project còn lại dùng setting global.

8

Use Cases

Khi nào dùng fullyParallel: true

  • Test isolated: mỗi test tự setup và teardown state riêng qua beforeEach / afterEach.
  • Suite scale lớn (100+ test) cần fast feedback — tận dụng tối đa worker pool.
  • Greenfield project thiết kế test theo best practice ngay từ đầu.
  • Test không dùng beforeAll với resource cố định (port, DB unique constraint).

Khi nào giữ fullyParallel: false

  • Test trong file dùng beforeAll setup heavy và chia sẻ state với các test con — refactor sang beforeEach tốn nhiều effort.
  • Legacy suite có test phụ thuộc thứ tự trong file (test B rely on side effect từ test A).
  • File có beforeAll khởi động server/DB với resource cố định không thể dùng dynamic allocation.

CI/CD

Trên CI, tài nguyên thường giới hạn hơn local. Cấu hình phổ biến:

// playwright.config.ts
export default defineConfig({
  fullyParallel: true,
  workers: process.env.CI ? 2 : 4,
});

Với 2 worker trên CI và fullyParallel: true, test vẫn được phân bổ test-level thay vì file-level — tốt hơn so với false + 2 worker khi các file có kích thước không đều. Kết hợp với sharding (bài 70) để phân tán test lên nhiều machine CI.

9

Migration Pattern

Migration an toàn từ fullyParallel: false sang true cho legacy suite:

Bước 1: Bật fullyParallel: true

export default defineConfig({
  fullyParallel: true,
  workers: 4,
});

Bước 2: Chạy suite, quan sát lỗi

npx playwright test --reporter=list

Các lỗi thường gặp sau bước này:

  • Test fail do đọc/ghi shared module-level variable.
  • beforeAll setup resource cố định → conflict khi nhiều worker cùng chạy.
  • Test B fail vì side effect từ test A không còn đảm bảo chạy trước.

Bước 3: Tạm thời mark describe vi phạm thành serial

// Tìm describe có test fail → mark serial tạm thời
test.describe('Legacy checkout', () => {
  test.describe.configure({ mode: 'serial' }); // TODO: remove after refactor

  test('step 1', async () => { /* ... */ });
  test('step 2', async () => { /* ... */ });
});

Suite sẽ pass lại. Các describe không bị mark chạy parallel bình thường.

Bước 4: Refactor từng describe

Với từng describe đang ở serial:

  • Chuyển shared state từ module-level / beforeAll sang beforeEach.
  • Mỗi test tự tạo data riêng, không rely on order.
  • Dùng dynamic resource (port 0, unique DB prefix per parallelIndex).

Bước 5: Bỏ mark serial

Sau khi refactor xong, xóa describe.configure({ mode: 'serial' }) và chạy lại để xác nhận pass.

10

Pitfalls

1. Shared module-level variable

// SSCG — lỗi khi fullyParallel: true
let loggedInUserId: string;

test.beforeAll(async ({ request }) => {
  // Worker 1 set loggedInUserId = "user-abc"
  // Worker 2 set loggedInUserId = "user-xyz"  ← ghi đè
  const res = await request.post('/api/login');
  loggedInUserId = (await res.json()).id;
});

test('view dashboard', async ({ page }) => {
  // loggedInUserId có thể bị worker khác ghi đè giữa chừng
  await page.goto(`/dashboard/${loggedInUserId}`);
});

Sửa: chuyển loggedInUserId vào scope test qua beforeEach hoặc fixture.

2. Port hardcode trong beforeAll

// SAI: 2 worker cùng bind port 3000 → EADDRINUSE
test.beforeAll(async () => {
  await startMockServer({ port: 3000 });
});

// Đúng: dùng port 0 — OS tự assign
test.beforeAll(async () => {
  mockServer = await startMockServer({ port: 0 });
  mockPort = mockServer.address().port; // mỗi worker có port riêng
});

3. beforeAll seed DB với unique constraint

// SAI: 2 worker cùng insert email '[email protected]' → constraint error
test.beforeAll(async () => {
  await db.insert({ email: '[email protected]', role: 'admin' });
});

// Đúng: dùng parallelIndex để tạo unique data
test.beforeAll(async ({}, testInfo) => {
  const idx = testInfo.parallelIndex;
  await db.insert({ email: `admin_${idx}@test.com`, role: 'admin' });
});

4. Quên bật fullyParallel: true trên CI

Suite local chạy đủ worker, nhưng CI dùng config cũ không có fullyParallel: true. Kết quả: suite chậm hơn cần thiết, feedback loop dài. Giải pháp: kiểm tra playwright.config.ts trong repo, không dùng config override riêng trên CI trừ khi có lý do rõ ràng.

5. Nhầm fullyParallel với số worker

fullyParallel: true chỉ thay đổi đơn vị phân phối (test thay vì file), không tăng số worker. Nếu workers: 1, dù fullyParallel: true, test vẫn chạy tuần tự vì chỉ có 1 worker xử lý. Hai option này phải kết hợp với nhau để đạt throughput cao.

11

Tổng Kết

  • fullyParallel: false: file = 1 worker, test trong file chạy serial.
  • fullyParallel: true: mỗi test được phân bổ vào worker available bất kỳ — test isolation là bắt buộc.
  • beforeAll chạy trên mỗi worker sử dụng file đó — tránh resource cố định (port, unique DB key).
  • Kết hợp fullyParallel: true + describe.configure({ mode: 'serial' }) cho describe có state phụ thuộc.
  • Override per project qua projects[].fullyParallel khi các nhóm test có yêu cầu khác nhau.
  • Migration: bật true → quan sát lỗi → mark serial tạm thời → refactor → bỏ mark.
  • fullyParallel: true không tự tăng tốc nếu workers: 1 — cần đủ worker.
12

Quiz

Câu 1. Config fullyParallel: false, workers: 3, suite có 3 file mỗi file 5 test. Tối đa bao nhiêu test chạy đồng thời?

Xem đáp án

3 test — mỗi worker nhận 1 file, chạy đúng 1 test của file đó tại một thời điểm. Test trong file vẫn serial.

Câu 2. fullyParallel: true, workers: 2. File có beforeAll khởi động server port: 4000. Suite báo EADDRINUSE. Nguyên nhân?

Xem đáp án

2 worker cùng chạy test từ file đó, cả 2 đều thực thi beforeAll và cố bind cùng port 4000. Sửa bằng cách dùng port: 0 để OS tự assign port riêng cho mỗi worker.

Câu 3. Dự án có fullyParallel: true global. Describe "Login flow" gồm 3 test phụ thuộc thứ tự. Cần làm gì để đảm bảo chúng chạy tuần tự?

Xem đáp án

Thêm test.describe.configure({ mode: 'serial' }) bên trong describe "Login flow". Option này override fullyParallel ở scope describe.

Câu 4. fullyParallel: true, workers: 1. Test có chạy song song không?

Xem đáp án

Không. Chỉ có 1 worker — dù đơn vị phân phối là test, vẫn chỉ 1 test chạy tại một thời điểm. Cần workers >= 2 để song song.

Câu 5. beforeAll insert row email: '[email protected]' vào DB với unique constraint. fullyParallel: true, workers: 3. Điều gì xảy ra khi chạy suite?

Xem đáp án

3 worker đều thực thi beforeAll → 3 lần insert cùng email → lần thứ 2 và 3 vi phạm unique constraint → lỗi. Sửa bằng cách dùng testInfo.parallelIndex để tạo email unique: `seed_${testInfo.parallelIndex}@test.com`.