Mục lục
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: falsevàfullyParallel: trueở mức worker assignment. - Biết tại sao
fullyParallel: trueyêu cầu test phải isolated. - Nắm rõ behavior của
beforeAllkhi bậtfullyParallel. - 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.
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.
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 đó |
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ặctest.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.
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
beforeAllnặng. - Port conflict: nếu
beforeAllkhởi động HTTP server trên port cố định, 2 worker chạy đồng thời → conflict ngay. - DB seed duplicate: nếu
beforeAllinsert 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}_` });
});
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.
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.
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
beforeAllvới resource cố định (port, DB unique constraint).
Khi nào giữ fullyParallel: false
- Test trong file dùng
beforeAllsetup heavy và chia sẻ state với các test con — refactor sangbeforeEachtố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ó
beforeAllkhở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.
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.
beforeAllsetup 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 /
beforeAllsangbeforeEach. - Mỗi test tự tạo data riêng, không rely on order.
- Dùng dynamic resource (port
0, unique DB prefix perparallelIndex).
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.
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.
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.beforeAllchạ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[].fullyParallelkhi 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: truekhông tự tăng tốc nếuworkers: 1— cần đủ worker.
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`.
