Danh sách bài viết

Bài 86: Timeout Cho beforeAll / afterAll

beforeAll và afterAll chạy ở worker-scope — timeout của chúng tồn tại độc lập, không cộng vào test timeout của bất kỳ test nào trong scope. Mặc định Playwright dùng giá trị test timeout (30s) cho cả hook lẫn test. Bài này phân tích sự khác biệt giữa hook timeout worker-scope (beforeAll/afterAll) và hook timeout test-scope (beforeEach/afterEach), cách set hook timeout bằng test.setTimeout() bên trong hook, cách dùng describe.configure, behavior khi hook timeout, và 4 pitfall phổ biến. Bài 33 đã cover lifecycle đầy đủ của beforeAll worker-scope — bài này chỉ tập trung vào khía cạnh timeout.

28/05/2026
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ẽ:

  • 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() trong beforeEach để 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.
2

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');
});
3

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.

4

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() trong beforeAll → 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ó.

5

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.

6

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.configure phải nằm ở đầu describe block, trước bất kỳ test.beforeAll hay test() nào.
  • Timeout từ describe.configure áp dụng cho beforeAll lẫn afterAll trong block đó — không cần gọi test.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');
  });
});
7

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ý.

8

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.

9

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
10

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');
  });
});
11

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) trong beforeEach tă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-hook test.setTimeout() ghi đè describe.configure.
  • Khi beforeAll timeout: tất cả test trong scope bị skip, run tiếp tục.
  • Khi afterAll timeout: cleanup incomplete, test results không đổi, Playwright log error.
  • Hook timeout khác globalTimeout (toàn run) và test timeout (1 test).
12

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 Atest 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.

13

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.

Bài 87: test.setTimeout() Tại Runtime Trong Test Body