Mục lục
- Mục Tiêu Bài Học
- Hook Timeout: Worker-Scope vs Test-Scope
- Mặc Định: Hook Timeout = Test Timeout
- Set Hook Timeout Bằng test.setTimeout()
- testInfo.setTimeout() Trong beforeEach
- describe.configure — Timeout Cho Cả Nhóm
- Use Case: Setup Nặng Trong beforeAll
- afterAll Timeout — Cleanup Nặng
- Behavior Khi Hook Fail Do Timeout
- 4 Pitfall Thực Tế
- Tổng Kết
- Quiz Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau khi hoàn thành bài này, bạn sẽ:
- Phân biệt hook timeout worker-scope (
beforeAll/afterAll) với hook timeout test-scope (beforeEach/afterEach). - Hiểu mặc định hook timeout là bao nhiêu và tại sao 30s thường không đủ cho setup nặng.
- Set hook timeout đúng cách bằng
test.setTimeout()bên trong hook. - Dùng
testInfo.setTimeout()trongbeforeEachđể tăng test timeout cho test sắp chạy. - Dùng
test.describe.configure({ timeout })để apply timeout cho cả nhóm hook. - Xử lý đúng khi hook fail do timeout — tránh để cleanup incomplete.
Hook Timeout: Worker-Scope vs Test-Scope
Playwright có 4 hook chính, chia làm 2 nhóm theo scope:
| Hook | Scope | Chạy khi nào | Timeout tính vào đâu |
|---|---|---|---|
beforeAll |
Worker | 1 lần per worker trước test đầu tiên trong scope | Riêng — KHÔNG cộng vào test timeout |
afterAll |
Worker | 1 lần per worker sau test cuối trong scope | Riêng — KHÔNG cộng vào test timeout |
beforeEach |
Test | Trước mỗi test | Cộng vào test timeout (dùng chung budget) |
afterEach |
Test | Sau mỗi test | Cộng vào test timeout (dùng chung budget) |
Đây là điểm khác biệt then chốt: beforeAll/afterAll có đồng hồ riêng không liên quan đến test nào cụ thể. beforeEach/afterEach ăn chung budget với test đang chạy.
Ví dụ minh họa tác động thực tế:
// beforeEach tốn 5s → budget của test chỉ còn 25s (nếu test timeout = 30s)
test.beforeEach(async ({ page }) => {
await page.goto('/app'); // 5s
});
test('checkout', async ({ page }) => {
// test body chỉ còn 25s budget
await page.click('#checkout'); // nếu click này mất 26s → test timeout
});
// beforeAll tốn 20s → test đầu tiên vẫn có đầy đủ 30s budget
test.beforeAll(async () => {
await seedDatabase(); // 20s — hoàn toàn tách biệt khỏi test timeout
});
test('checkout', async ({ page }) => {
// test này có đầy đủ 30s budget dù beforeAll đã chạy 20s
await page.click('#checkout');
});
Mặc Định: Hook Timeout = Test Timeout
Khi không set gì thêm, Playwright dùng giá trị timeout từ config cho cả hook lẫn test. Nếu config là 30 000 ms, mỗi hook có tối đa 30s để hoàn thành — giống test body.
Điều này hợp lý cho hook nhẹ (login, navigate), nhưng là vấn đề với hook nặng:
- Seed database với 100 000 records — thường mất 60-120s.
- Khởi động Docker container (testcontainers) — 30-90s tùy image.
- Compile asset lớn — có thể mất vài phút.
- Connect external service có cold start — 45-60s.
Với 30s mặc định, tất cả use case trên đều sẽ timeout. Hook bị kill, test không bao giờ chạy, và error message sẽ là "Test timeout of 30000ms exceeded" — không phải lỗi trong test body mà lỗi trong hook setup.
// playwright.config.ts — chỉ set test timeout
export default defineConfig({
timeout: 30_000, // 30s — áp dụng cho cả test LẪN hook (nếu không override)
});
// Hook này sẽ fail nếu seedLargeDataset() mất hơn 30s
test.beforeAll(async () => {
await seedLargeDataset(); // thực tế mất 60s → hook timeout sau 30s
});
Cần tường minh set timeout cao hơn cho các hook nặng — đây là lý do chính của bài này.
Set Hook Timeout Bằng test.setTimeout()
Cách trực tiếp nhất để set timeout cho một hook cụ thể: gọi test.setTimeout() ngay trong thân hook. Giá trị này chỉ áp dụng cho hook đó, không lan sang test hay hook khác.
test.beforeAll(async () => {
test.setTimeout(120_000); // 2 phút cho hook này
await seedLargeDataset();
});
Hành vi chi tiết:
test.setTimeout()trongbeforeAll→ set timeout cho hook đó (worker-scope). Không ảnh hưởng test timeout.- Gọi ở đầu hook, trước bất kỳ async operation nào, để đảm bảo timeout được set trước khi bất kỳ await nào bắt đầu.
- Timeout tính từ thời điểm hook bắt đầu chạy (không phải từ lúc gọi
setTimeout).
Pattern đầy đủ cho beforeAll nặng:
import { test, expect } from '@playwright/test';
import { PostgreSqlContainer } from '@testcontainers/postgresql';
let container: StartedPostgreSqlContainer;
test.beforeAll(async () => {
test.setTimeout(180_000); // 3 phút — đủ cho container start + migration
container = await new PostgreSqlContainer().start();
await runMigrations(container.getConnectionUri());
});
test.afterAll(async () => {
test.setTimeout(60_000); // 1 phút cho cleanup
await container.stop();
});
test('user registration', async ({ page }) => {
// test này vẫn có full 30s (từ config) — không bị ảnh hưởng bởi beforeAll timeout
await page.goto('/register');
await page.fill('#email', '[email protected]');
await page.click('#submit');
await expect(page.getByText('Welcome')).toBeVisible();
});
Lưu ý: khi gọi test.setTimeout() trong beforeAll, Playwright áp dụng timeout đó cho hook execution hiện tại (1 lần per worker). Nếu có 3 worker chạy song song, mỗi worker có beforeAll riêng với timeout riêng của nó.
testInfo.setTimeout() Trong beforeEach
beforeEach không có timeout riêng — nó dùng chung budget với test. Khi dùng test.setTimeout() bên trong beforeEach, nó set lại test timeout (bao gồm cả phần hook đang chạy) cho test sắp chạy.
Tuy nhiên có một cú pháp khác hữu ích hơn: testInfo.setTimeout() với delta:
test.beforeEach(async ({}, testInfo) => {
// Tăng thêm 30s vào timeout hiện tại của test
testInfo.setTimeout(testInfo.timeout + 30_000);
await page.goto('/app'); // tốn 5s trong beforeEach → test còn nhiều budget hơn
});
So sánh hai cách:
| Cách | Hành vi | Khi nào dùng |
|---|---|---|
test.setTimeout(N) trong beforeEach |
Set test timeout = N (tuyệt đối), tính từ khi test bắt đầu (bao gồm cả beforeEach đang chạy) | Muốn ghi đè hoàn toàn test timeout cho test này |
testInfo.setTimeout(testInfo.timeout + delta) |
Tăng thêm delta ms vào timeout hiện tại |
Muốn bù thêm cho beforeEach tốn thời gian, giữ nguyên budget test body |
Trường hợp phổ biến: beforeEach cần login API call tốn 5-10s, muốn test body vẫn có đủ 30s:
test.beforeEach(async ({ request }, testInfo) => {
// Login mất ~8s → tăng thêm 15s buffer để test body vẫn đủ budget
testInfo.setTimeout(testInfo.timeout + 15_000);
const token = await request.post('/api/login', {
data: { email: '[email protected]', password: 'secret' },
});
process.env.AUTH_TOKEN = (await token.json()).token;
});
Lưu ý quan trọng: vì beforeEach dùng chung budget với test, testInfo.setTimeout() ảnh hưởng toàn bộ test (hook + body + afterEach). Không thể set timeout riêng cho chỉ phần beforeEach như beforeAll.
describe.configure — Timeout Cho Cả Nhóm
test.describe.configure({ timeout }) set timeout mặc định cho tất cả test và hook trong describe block đó. Đây là cách gọn hơn khi cả beforeAll lẫn test trong nhóm đều cần timeout cao hơn config global:
test.describe('Heavy database tests', () => {
test.describe.configure({ timeout: 60_000 }); // áp dụng cho tất cả trong block này
// beforeAll có 60s (từ configure) thay vì 30s (config global)
test.beforeAll(async () => {
await seedLargeDataset(); // mất 45s → OK với 60s
});
// afterAll cũng có 60s
test.afterAll(async () => {
await cleanupDatabase();
});
// Mỗi test cũng có 60s timeout
test('read all records', async ({ page }) => {
await page.goto('/admin/records');
await expect(page.getByRole('row')).toHaveCount(100_000);
});
});
Điểm cần lưu ý:
describe.configurephải nằm ở đầu describe block, trước bất kỳtest.beforeAllhaytest()nào.- Timeout từ
describe.configureáp dụng chobeforeAlllẫnafterAlltrong block đó — không cần gọitest.setTimeout()riêng trong từng hook. - Nếu bên trong hook vẫn gọi
test.setTimeout(), giá trị đó ghi đèdescribe.configure. - Describe lồng nhau:
describe.configureở outer describe không tự động áp dụng cho inner describe — inner phải configure riêng hoặc kế thừa từ outer (tùy version, nên khai báo tường minh).
Pattern kết hợp describe.configure với per-hook override:
test.describe('Integration suite', () => {
test.describe.configure({ timeout: 60_000 }); // mặc định 60s cho nhóm
test.beforeAll(async () => {
// Hook này cần nhiều hơn 60s → override riêng
test.setTimeout(120_000); // ghi đè describe.configure cho hook này
await startDockerStack(); // mất ~90s
});
test.afterAll(async () => {
// Hook này OK với 60s từ describe.configure — không cần override
await stopDockerStack();
});
test('create order', async ({ page }) => {
// Test này có 60s từ describe.configure
await page.goto('/orders/new');
});
});
Use Case: Setup Nặng Trong beforeAll
Các tình huống thực tế cần tăng timeout cho beforeAll:
Seed database lớn
test.beforeAll(async () => {
test.setTimeout(120_000); // 2 phút
await db.truncate(['orders', 'products', 'users']);
await db.seed('fixtures/large-dataset.sql'); // 100k rows, mất ~80s
});
Spawn Docker container (testcontainers)
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { RedisContainer } from '@testcontainers/redis';
let pgContainer: StartedPostgreSqlContainer;
let redisContainer: StartedRedisContainer;
test.beforeAll(async () => {
test.setTimeout(180_000); // 3 phút — pull image lần đầu có thể lâu
[pgContainer, redisContainer] = await Promise.all([
new PostgreSqlContainer('postgres:16').start(),
new RedisContainer('redis:7').start(),
]);
process.env.DATABASE_URL = pgContainer.getConnectionUri();
process.env.REDIS_URL = `redis://localhost:${redisContainer.getMappedPort(6379)}`;
});
Compile asset trước khi test
import { execSync } from 'child_process';
test.beforeAll(async () => {
test.setTimeout(300_000); // 5 phút cho cold build
execSync('npm run build:test', { stdio: 'inherit' });
});
Connect external service có cold start
test.beforeAll(async () => {
test.setTimeout(90_000); // 90s cho service warm-up
let retries = 0;
while (retries < 10) {
try {
await checkServiceHealth('https://staging.example.com/health');
break;
} catch {
retries++;
await new Promise(r => setTimeout(r, 5000)); // chờ 5s giữa retry
}
}
});
Điểm chung: timeout phải lớn hơn thời gian thực tế worst-case của operation, cộng buffer. Measure thực tế rồi nhân 1.5-2x là hợp lý.
afterAll Timeout — Cleanup Nặng
afterAll cũng là worker-scope hook — timeout riêng, không ảnh hưởng test. Tuy nhiên cleanup có đặc thù riêng: nếu afterAll timeout, cleanup không hoàn thành, dẫn đến resource leak.
Các cleanup cần timeout cao:
- Drop schema và truncate table lớn.
- Stop Docker container.
- Flush file buffer lớn ra disk.
- Close connection pool.
test.afterAll(async () => {
test.setTimeout(60_000); // 1 phút để cleanup container + schema
// Không dùng Promise.all cho cleanup — nên stop từng resource
// để nếu 1 fail vẫn có thể chạy tiếp resource khác
try {
await container.stop();
} catch (err) {
console.error('Container stop failed:', err);
}
try {
await db.dropSchema('test_schema');
} catch (err) {
console.error('Schema drop failed:', err);
}
});
Lý do không dùng Promise.all trong cleanup: nếu container.stop() throw, phần còn lại của cleanup không chạy → schema không được drop → test run tiếp theo bị ảnh hưởng.
Nếu cleanup timeout, Playwright log error nhưng vẫn tiếp tục các test/file khác. Resource leak lúc này cần được phát hiện qua log CI, không phải qua test fail.
Behavior Khi Hook Fail Do Timeout
Behavior khác nhau tùy hook nào fail:
beforeAll timeout
Tất cả test trong scope bị skip (không phải fail — trạng thái là skipped). Playwright không cố chạy test nếu setup chưa hoàn thành:
Error: beforeAll hook timeout of 30000ms exceeded.
1) [webkit] › integration.spec.ts › user registration — skipped
Setup failed, skipping all tests.
2) [webkit] › integration.spec.ts › login flow — skipped
Setup failed, skipping all tests.
Điều này có nghĩa là một beforeAll timeout sẽ làm cả nhóm test "không có kết quả" — không đếm vào fail count nhưng cũng không đếm vào pass. CI có thể pass nếu 0 fail nhưng có nhiều test bị skip.
afterAll timeout
Tất cả test trong scope đã chạy và có kết quả bình thường. afterAll timeout không làm test fail retroactively. Playwright log error:
Error: afterAll hook timeout of 30000ms exceeded while cleaning up.
↳ Cleanup may be incomplete. Check for resource leaks.
Test results vẫn giữ nguyên (pass là pass, fail là fail). Chỉ có cleanup bị incomplete.
Phân biệt với globalTimeout
Bài 82 đã cover globalTimeout — giới hạn cho toàn bộ run. Hook timeout (dù beforeAll hay afterAll) là giới hạn cho 1 hook execution cụ thể, hoàn toàn khác với globalTimeout:
| Loại timeout | Scope | Khi exceed |
|---|---|---|
globalTimeout |
Toàn bộ run | Playwright dừng tất cả, exit ngay |
Hook timeout (beforeAll) |
1 hook invocation | Test trong scope bị skip, run tiếp tục |
Hook timeout (afterAll) |
1 hook invocation | Cleanup incomplete, run tiếp tục |
| Test timeout | 1 test | Test fail với timedOut, run tiếp tục |
4 Pitfall Thực Tế
1. Seed nặng trong beforeAll không tăng timeout
// SAI — quên set timeout, hook bị kill sau 30s
test.beforeAll(async () => {
await seedLargeDataset(); // mất 60s → timeout sau 30s → test bị skip
});
// ĐÚNG — set timeout trước operation nặng
test.beforeAll(async () => {
test.setTimeout(120_000); // 2 phút
await seedLargeDataset(); // có đủ thời gian
});
2. Nhầm beforeAll timeout (riêng) với beforeEach timeout (đếm vào test)
// beforeAll: gọi test.setTimeout() trong hook → chỉ ảnh hưởng hook đó
test.beforeAll(async () => {
test.setTimeout(120_000); // set cho hook beforeAll này, test vẫn có 30s
await heavySetup();
});
// beforeEach: gọi test.setTimeout() → set toàn bộ test timeout (hook + body + afterEach)
test.beforeEach(async () => {
test.setTimeout(60_000); // set cho CẢ test (beforeEach + body + afterEach) = 60s
await mediumSetup(); // mất 10s → body còn 50s budget
});
// Nhầm lẫn: nghĩ beforeEach có timeout riêng 60s + test body có 30s riêng
// Thực tế: 60s là budget CHUNG cho toàn bộ test từ lúc beforeEach bắt đầu
3. afterAll cleanup chậm timeout → cleanup incomplete
// SAI — không set timeout cho cleanup container nặng
test.afterAll(async () => {
await container.stop(); // mất 45s → timeout sau 30s
await db.dropSchema(); // không bao giờ chạy đến đây
// → schema vẫn còn, container process zombie
});
// ĐÚNG — set timeout và handle lỗi từng bước
test.afterAll(async () => {
test.setTimeout(60_000);
try {
await container.stop();
} catch (e) {
console.error('container.stop() failed:', e);
}
try {
await db.dropSchema();
} catch (e) {
console.error('dropSchema() failed:', e);
}
});
4. Nghĩ test.setTimeout() trong beforeAll ảnh hưởng cả describe
test.beforeAll(async () => {
test.setTimeout(120_000);
await heavySetup();
});
// Nhầm: nghĩ timeout 120s áp dụng cho mọi test trong describe
// Thực tế: 120s CHỈ áp dụng cho hook beforeAll đó
// Mỗi test vẫn có timeout = config (30s)
test('test A', async ({ page }) => {
// timeout vẫn là 30s, không phải 120s
await page.goto('/heavy-page'); // nếu load mất 35s → test timeout, không phải 120s
});
// ĐÚNG — dùng describe.configure nếu muốn apply cho cả nhóm
test.describe('Heavy group', () => {
test.describe.configure({ timeout: 120_000 }); // áp dụng cho hook VÀ test
test.beforeAll(async () => {
await heavySetup(); // có 120s
});
test('test A', async ({ page }) => {
// cũng có 120s
await page.goto('/heavy-page');
});
});
Tổng Kết
beforeAll/afterAll(worker-scope) có timeout riêng — không cộng vào test timeout của bất kỳ test nào.beforeEach/afterEach(test-scope) dùng chung budget với test đang chạy.- Mặc định hook timeout = test timeout từ config (30s) — không đủ cho setup nặng.
- Set hook timeout bằng
test.setTimeout(N)ngay đầu hook — chỉ áp dụng cho hook đó, không ảnh hưởng test. testInfo.setTimeout(testInfo.timeout + delta)trongbeforeEachtăng thêm budget cho toàn bộ test (hook + body + afterEach).test.describe.configure({ timeout })set timeout mặc định cho tất cả hook và test trong block. Per-hooktest.setTimeout()ghi đèdescribe.configure.- Khi
beforeAlltimeout: tất cả test trong scope bị skip, run tiếp tục. - Khi
afterAlltimeout: cleanup incomplete, test results không đổi, Playwright log error. - Hook timeout khác
globalTimeout(toàn run) và test timeout (1 test).
Quiz Củng Cố
Câu 1
Config có timeout: 30_000. Hook sau chạy mất 45s. Điều gì xảy ra với hook và các test trong scope?
test.beforeAll(async () => {
await seedLargeDataset(); // mất 45s
});
test('test A', async ({ page }) => { /* ... */ });
test('test B', async ({ page }) => { /* ... */ });
Đáp án
Hook bị kill sau 30s (timeout từ config). Cả test A và test B đều bị skip — không phải fail. Playwright không chạy test nếu beforeAll chưa hoàn thành.
Fix: thêm test.setTimeout(60_000); ở đầu beforeAll.
Câu 2
Hai đoạn code sau khác nhau thế nào về timeout budget cho test body?
// Code A
test.beforeEach(async () => {
test.setTimeout(60_000);
await login(); // mất 10s
});
// Code B
test.beforeEach(async ({}, testInfo) => {
testInfo.setTimeout(testInfo.timeout + 30_000);
await login(); // mất 10s
});
Đáp án
Code A: test timeout được set = 60s, tính từ lúc beforeEach bắt đầu. Sau khi login() mất 10s, test body còn ~50s budget.
Code B: test timeout được tăng thêm 30s vào giá trị hiện tại (default 30s → 60s). Kết quả tương đương Code A trong trường hợp này, nhưng Code B có ưu điểm: nếu config thay đổi timeout (vd thành 45s), Code B tự động tăng thêm 30s (tổng = 75s), còn Code A luôn cố định 60s bất kể config.
Câu 3
beforeAll dưới đây có timeout bao nhiêu giây?
test.describe('Integration', () => {
test.describe.configure({ timeout: 60_000 });
test.beforeAll(async () => {
test.setTimeout(180_000);
await startDockerStack();
});
});
Đáp án
180s. test.setTimeout(180_000) bên trong hook ghi đè describe.configure({ timeout: 60_000 }). Per-hook override luôn có độ ưu tiên cao hơn describe.configure.
Câu 4
afterAll sau timeout sau 30s khi đang container.stop(). Điều gì xảy ra với kết quả các test đã chạy trong scope?
Đáp án
Kết quả các test không thay đổi. afterAll timeout không làm test retroactively fail hay skip. Playwright log error "afterAll hook timeout exceeded" và tiếp tục các file test khác. Cleanup bị incomplete — container.stop() chưa hoàn thành, container process có thể vẫn còn chạy.
Đây là lý do cần set test.setTimeout() đủ lớn trong afterAll và handle từng cleanup step bằng try/catch riêng.
Bài Tiếp Theo
Bài 87 tiếp tục nhóm Timeouts với test.setTimeout() dùng tại runtime trong test body — cách thay đổi test timeout cho một test cụ thể đang chạy.
