Mục lục
- Mục Tiêu Bài Học
- Cú Pháp Và Vị Trí Khai Báo
- Behavior Chi Tiết
- Fixture Khả Dụng
- Idempotent Cleanup
- Multiple afterAll — LIFO Order
- Pattern Combine beforeAll + afterAll
- Pattern Với Mock Server
- Nested Describe — Thứ Tự Thực Thi
- Hook Fail Trong afterAll
- Timeout
- Order Với Worker Fixture Cleanup
- So Sánh Với globalTeardown
- Limitation
- Common Pitfalls
- Tổng Kết
- Quiz
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài này bạn sẽ:
- Giải thích chính xác khi nào
afterAllchạy và khi nào nó không chạy. - Viết cleanup idempotent — an toàn khi
beforeAllfail giữa chừng và state là partial. - Hiểu LIFO order khi khai báo nhiều
afterAlltrong cùng scope. - Áp dụng đúng pattern combine
beforeAll+afterAllcho DB pool và mock server. - Biết timeout mặc định của hook và cách override.
- Phân biệt
afterAllvớiglobalTeardown. - Nhận diện 4 pitfall phổ biến và cách tránh.
Bài này không lặp lại nội dung bài 400 Series 1 (cú pháp cơ bản, worker scope). Focus vào các behavior nâng cao, defensive coding, và cleanup pattern thực tế.
Cú Pháp Và Vị Trí Khai Báo
import { test, expect } from '@playwright/test';
test.afterAll(async () => {
await cleanupSeededData();
});
Hook nhận callback async, trả về Promise<void>. Playwright đợi promise resolve trước khi kết thúc scope.
Có thể khai báo ở hai vị trí:
- Top-level: chạy sau tất cả test trong file.
- Trong describe: chỉ chạy sau tất cả test trong describe đó.
// Top-level — áp dụng toàn file
test.afterAll(async () => {
await dropTestSchema(); // cleanup cho mọi test trong file
});
// Trong describe — chỉ áp dụng cho group
test.describe('Checkout flow', () => {
test.afterAll(async () => {
await deleteOrdersByPrefix('TEST-');
});
test('creates order', async ({ page }) => { /* ... */ });
test('cancels order', async ({ page }) => { /* ... */ });
});
// Test này không bị ảnh hưởng bởi afterAll trong describe
test('homepage loads', async ({ page }) => { /* ... */ });
Behavior Chi Tiết
Bốn điểm behavior quan trọng cần nắm rõ:
1. Chạy sau test cuối của scope
Không phải sau mỗi test mà sau khi test cuối cùng trong file hoặc describe hoàn thành.
2. Chạy kể cả khi test fail
Dù 1 hay nhiều test trong scope throw error, afterAll vẫn được gọi. Đây là lý do afterAll phù hợp cho cleanup — không phụ thuộc vào kết quả test.
test.afterAll(async () => {
// Chạy ngay cả khi test 1, test 2, test 3 đều fail
await cleanupSeededData();
});
test('test 1', async ({ page }) => {
throw new Error('intentional fail');
});
test('test 2', async ({ page }) => { /* ... */ });
3. Chạy kể cả khi beforeAll fail
Đây là điểm dễ bị bỏ qua. Nếu beforeAll thực hiện 3 bước và fail ở bước 2, state lúc này là partial — bước 1 đã xong, bước 3 chưa chạy. afterAll vẫn được gọi sau đó. Cleanup code phải xử lý được trạng thái partial này.
let pool: Pool;
let schemaCreated = false;
test.beforeAll(async () => {
pool = await createPool(); // bước 1: OK
await createSchema(pool); // bước 2: fail tại đây
await seedData(pool); // bước 3: không chạy
schemaCreated = true;
});
test.afterAll(async () => {
// afterAll vẫn chạy dù beforeAll fail ở bước 2
// pool đã tạo nhưng schema chưa có, seedData chưa chạy
// Cần handle partial state
if (schemaCreated) {
await dropSchema(pool).catch(err => console.warn('dropSchema:', err));
}
await pool?.end(); // optional chain phòng pool chưa được assign
});
4. Worker scope
afterAll là worker-scope hook — chạy 1 lần per worker. Với fullyParallel: true (mặc định), mỗi file chạy trên 1 worker riêng nên afterAll chạy đúng 1 lần per file.
Fixture Khả Dụng
afterAll chạy ở worker scope — giới hạn fixture giống beforeAll:
| Fixture | Khả dụng | Lý do |
|---|---|---|
browser |
Có | Worker-scope built-in |
playwright |
Có | Worker-scope built-in |
| Custom worker-scope fixture | Có | Cùng vòng đời |
page |
Không | Test-scope — không tồn tại ngoài test |
context |
Không | Test-scope — tương tự page |
| Custom test-scope fixture | Không | Test-scope — không tồn tại ngoài test |
Nếu cleanup cần navigate (ví dụ gọi admin endpoint qua browser), tạo page thủ công từ browser rồi close:
test.afterAll(async ({ browser }) => {
const page = await browser.newPage();
await page.goto('/admin/cleanup');
await page.getByRole('button', { name: 'Reset Test Data' }).click();
await page.close();
});
Trong thực tế, cleanup nên dùng direct API call (DB client, HTTP request) thay vì browser navigation để giảm flakiness và không phụ thuộc UI.
Idempotent Cleanup
Cleanup idempotent là cleanup an toàn khi gọi nhiều lần hoặc khi resource chưa được tạo đầy đủ. Vì afterAll chạy kể cả khi beforeAll fail, cleanup code không thể assume rằng mọi thứ đã sẵn sàng.
Pattern cơ bản — try/catch
test.afterAll(async () => {
try {
await dropTestSchema();
} catch (err) {
// Schema có thể chưa được tạo (beforeAll fail trước khi tạo schema)
console.warn('Schema already dropped or never created:', err);
// Không re-throw — cleanup error không nên làm fail test
}
});
Pattern với flag trạng thái
Khi cleanup phức tạp và phụ thuộc vào trạng thái setup:
let userId: number | null = null;
let orderIds: number[] = [];
test.beforeAll(async () => {
userId = await db.createTestUser({ email: '[email protected]' });
orderIds = await db.createTestOrders(userId, 3);
});
test.afterAll(async () => {
// Cleanup theo thứ tự ngược với setup (FK constraint)
if (orderIds.length > 0) {
await db.deleteOrders(orderIds).catch(err =>
console.warn('deleteOrders failed:', err)
);
}
if (userId !== null) {
await db.deleteUser(userId).catch(err =>
console.warn('deleteUser failed:', err)
);
}
});
SQL idempotent
Khi cleanup là SQL query, dùng IF EXISTS hoặc DELETE ... WHERE thay vì assume resource tồn tại:
test.afterAll(async () => {
// Idempotent — không throw nếu schema không tồn tại
await db.query('DROP SCHEMA IF EXISTS test_schema CASCADE');
await db.query("DELETE FROM users WHERE email LIKE 'test_%'");
});
Multiple afterAll — LIFO Order
Khi khai báo nhiều afterAll trong cùng scope, Playwright thực thi theo thứ tự LIFO (Last In, First Out) — hook khai báo sau chạy trước.
test.afterAll(async () => {
console.log('afterAll #1 — khai báo đầu tiên');
});
test.afterAll(async () => {
console.log('afterAll #2 — khai báo thứ hai');
});
test.afterAll(async () => {
console.log('afterAll #3 — khai báo cuối cùng');
});
// Output:
// afterAll #3 — khai báo cuối cùng
// afterAll #2 — khai báo thứ hai
// afterAll #1 — khai báo đầu tiên
LIFO order match với nguyên tắc cleanup stack: resource tạo sau thường phụ thuộc resource tạo trước, nên cần cleanup trước. Ví dụ: beforeAll #1 tạo DB pool, beforeAll #2 tạo schema dùng pool đó. Cleanup hợp lý: xoá schema trước, rồi đóng pool sau.
let pool: Pool;
let schemaName: string;
test.beforeAll(async () => { // #1 — setup pool
pool = await createPool();
});
test.beforeAll(async () => { // #2 — tạo schema (dùng pool)
schemaName = `test_${Date.now()}`;
await createSchema(pool, schemaName);
});
// LIFO: afterAll #2 chạy trước afterAll #1
test.afterAll(async () => { // #1 — đóng pool (chạy sau)
await pool?.end();
});
test.afterAll(async () => { // #2 — xoá schema (chạy trước, dùng pool còn mở)
await dropSchema(pool, schemaName).catch(err =>
console.warn('dropSchema:', err)
);
});
Nếu quên thứ tự LIFO và viết cleanup theo thứ tự FIFO, pool.end() có thể chạy trước khi schema bị drop — cleanup thứ hai sẽ fail vì pool đã đóng.
Pattern Combine beforeAll + afterAll
Pattern phổ biến nhất: biến module-level lưu resource tạo trong beforeAll, cleanup trong afterAll. Optional chain ?. là cần thiết để phòng trường hợp beforeAll fail trước khi assign biến.
DB connection pool
import { Pool } from 'pg';
import { test, expect } from '@playwright/test';
let pool: Pool;
test.beforeAll(async () => {
pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 5,
});
// Kiểm tra connection trước khi test chạy
const client = await pool.connect();
await client.query('SELECT 1');
client.release();
});
test.afterAll(async () => {
// Optional chain: pool chưa assign nếu beforeAll fail ngay dòng đầu
await pool?.end();
});
test('query users', async () => {
const client = await pool.connect();
const result = await client.query('SELECT count(*) FROM users');
client.release();
expect(Number(result.rows[0].count)).toBeGreaterThan(0);
});
Seeded data với cleanup
let productIds: number[] = [];
test.beforeAll(async () => {
productIds = await db.insertProducts([
{ name: 'Widget A', price: 9.99, sku: 'W-001' },
{ name: 'Widget B', price: 14.99, sku: 'W-002' },
{ name: 'Widget C', price: 19.99, sku: 'W-003' },
]);
});
test.afterAll(async () => {
if (productIds.length === 0) return; // beforeAll fail trước khi insert
await db.deleteProductsByIds(productIds).catch(err =>
console.warn('[afterAll] deleteProducts failed:', err)
);
});
Pattern Với Mock Server
Spin up mock server trong beforeAll, đóng trong afterAll. server.close() nhận callback — cần wrap trong Promise để await đúng cách:
import * as http from 'http';
import { test } from '@playwright/test';
let server: http.Server;
test.beforeAll(async () => {
server = http.createServer((req, res) => {
if (req.url === '/api/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok' }));
return;
}
res.writeHead(404);
res.end();
});
await new Promise<void>(resolve => server.listen(0, resolve));
// Port 0 → OS tự chọn port trống, tránh conflict
});
test.afterAll(async () => {
await new Promise<void>((resolve, reject) => {
server?.close((err) => (err ? reject(err) : resolve()));
});
});
test('health endpoint returns 200', async ({ request }) => {
const address = server.address() as { port: number };
const res = await request.get(`http://localhost:${address.port}/api/health`);
expect(res.ok()).toBe(true);
});
Dùng port 0 để tránh conflict port khi chạy nhiều file song song — OS tự gán port khả dụng. Lấy port thực tế qua server.address() sau khi listen.
Kill spawned process
Khi test cần spawn process ngoài (Docker container, external mock):
import { spawn, ChildProcess } from 'child_process';
let mockProcess: ChildProcess;
test.beforeAll(async () => {
mockProcess = spawn('node', ['test/mock-server.js'], {
env: { ...process.env, PORT: '3099' },
});
// Đợi server ready — đơn giản nhất là đợi stdout log
await new Promise<void>((resolve, reject) => {
mockProcess.stdout?.on('data', (data: Buffer) => {
if (data.toString().includes('listening')) resolve();
});
mockProcess.on('error', reject);
setTimeout(() => reject(new Error('mock server timeout')), 10_000);
});
});
test.afterAll(async () => {
mockProcess?.kill('SIGTERM');
// Đợi process exit để tránh orphan process
await new Promise<void>(resolve => {
mockProcess?.on('exit', () => resolve());
setTimeout(resolve, 3000); // fallback nếu process không exit
});
});
Nested Describe — Thứ Tự Thực Thi
Với nested describe, afterAll inner chạy trước outer — ngược với beforeAll:
test.beforeAll(async () => {
console.log('outer beforeAll');
});
test.afterAll(async () => {
console.log('outer afterAll');
});
test.describe('inner', () => {
test.beforeAll(async () => {
console.log('inner beforeAll');
});
test.afterAll(async () => {
console.log('inner afterAll');
});
test('inner case 1', async ({ page }) => {
console.log('inner test 1');
});
test('inner case 2', async ({ page }) => {
console.log('inner test 2');
});
});
test('outer case', async ({ page }) => {
console.log('outer test');
});
Thứ tự output:
outer beforeAll
inner beforeAll
inner test 1
inner test 2
inner afterAll
outer test
outer afterAll
Inner afterAll chạy ngay sau khi test cuối của describe đó hoàn thành — không phải sau outer test. Outer afterAll chạy sau tất cả mọi thứ trong file.
Hook Fail Trong afterAll
Khi afterAll throw error, behavior khác với beforeAll:
beforeAllthrow → test trong scope bị skip, báo là "did not run".afterAllthrow → không fail test đã chạy, chỉ log error và tiếp tục.
Điều này có nghĩa là cleanup failure không làm thay đổi kết quả test. Nhưng nó mask vấn đề cleanup, dẫn đến state leak qua các run sau.
Best practice: log error rõ ràng nhưng không re-throw từ cleanup. Nếu cleanup quan trọng và cần alert, log ra stderr hoặc dùng monitoring riêng:
test.afterAll(async () => {
try {
await dropTestSchema(schemaName);
await pool?.end();
} catch (err) {
// Log đủ thông tin để debug
console.error('[afterAll] cleanup failed:', {
schema: schemaName,
error: err instanceof Error ? err.message : err,
});
// Không re-throw — tránh mask test failure original
// Nếu cần alert, ghi vào file log riêng hoặc push metric
}
});
Nếu có nhiều bước cleanup, dùng Promise.allSettled hoặc xử lý từng bước riêng để một bước fail không chặn các bước còn lại:
test.afterAll(async () => {
const results = await Promise.allSettled([
dropTestSchema(schemaName),
deleteTestFiles(tmpDir),
redis?.quit(),
]);
results.forEach((result, i) => {
if (result.status === 'rejected') {
console.error(`[afterAll] cleanup step ${i} failed:`, result.reason);
}
});
});
Timeout
Mặc định, afterAll dùng chung timeout với test — thường 30 giây. Cleanup chậm (đóng pool nhiều connection, drop schema lớn) có thể vượt timeout này, dẫn đến partial cleanup.
Khi timeout xảy ra trong afterAll:
Error: afterAll hook timeout of 30000ms exceeded.
Override timeout bằng test.setTimeout() bên trong hook:
test.afterAll(async () => {
test.setTimeout(60_000); // 60 giây cho cleanup
await dropTestSchema(schemaName); // có thể mất 5-15s
await pool?.end(); // đợi connection drain
await clearMessageQueue(queueName); // flush queue
});
test.setTimeout() gọi trong afterAll chỉ áp dụng cho hook đó. Timeout của các test trong file không bị ảnh hưởng.
Nếu cleanup thường xuyên cần hơn 30 giây, nên xem lại thiết kế: có thể tách cleanup nặng ra globalTeardown, hoặc dùng database transaction rollback thay vì manual delete để cleanup nhanh hơn.
Order Với Worker Fixture Cleanup
Khi dùng cả worker-scope fixture và afterAll trong cùng worker, thứ tự cleanup là:
test N (test cuối)
→ test.afterEach (nếu có)
test.afterAll (cuối cùng trong scope)
worker fixture cleanup (teardown sau use())
Worker fixture cleanup chạy sau afterAll cuối cùng. Điều này quan trọng khi afterAll cần dùng resource từ worker fixture:
// fixtures.ts
export const test = base.extend<{}, { dbPool: Pool }>({
dbPool: [
async ({}, use) => {
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
await use(pool);
await pool.end(); // chạy SAU afterAll — OK
},
{ scope: 'worker' },
],
});
// spec.ts
import { test } from './fixtures';
test.afterAll(async ({ dbPool }) => {
// dbPool vẫn còn mở ở đây — worker fixture cleanup chưa chạy
await dbPool.query("DELETE FROM test_data WHERE session = 'test-run'");
});
Nếu afterAll cần worker fixture, inject trực tiếp qua destructuring parameter — Playwright hỗ trợ điều này.
So Sánh Với globalTeardown
| Đặc điểm | afterAll |
globalTeardown |
|---|---|---|
| Chạy khi nào | Sau test cuối của scope (file/describe) | 1 lần sau toàn bộ run, tất cả file |
| Số lần chạy | 1 lần per worker per scope | Đúng 1 lần duy nhất |
| Truy cập fixture | Có (worker-scope fixtures) | Không — chạy trong process riêng |
| Truy cập test result | Không trực tiếp | Có qua fullResult param |
| Phù hợp cho | Cleanup resource per file/describe | Cleanup toàn cục (truncate DB, xoá asset chung) |
Rule of thumb: nếu cleanup thuộc về file/group test cụ thể, dùng afterAll. Nếu cleanup thuộc về cả run (tạo bởi globalSetup), dùng globalTeardown.
Limitation
- Cleanup không guaranteed khi process bị kill: nếu worker bị SIGKILL (OOM, system crash),
afterAllkhông chạy. External resource (DB schema, file tạm, port) sẽ không được dọn dẹp. Cần cleanup strategy ngoài Playwright: script chạy trước run để dọn orphan resource từ run trước. - State leak qua run: nếu
afterAllfail hoặc bị skip (process kill), data test tồn tại sang run tiếp theo. Test sau có thể fail do unique constraint hoặc nhận data cũ. - Phụ thuộc beforeAll status: cleanup phải handle partial state khi
beforeAllfail — xem bài 3. Không có cách built-in để biết chính xácbeforeAllfail ở bước nào ngoài việc dùng flag. - Không access test result:
afterAllkhông biết test nào pass hay fail (khác vớiafterEachcótestInfo.status). Nếu cần conditional cleanup dựa trên kết quả test, dùngafterEachhoặc custom reporter.
Common Pitfalls
Pitfall 1: afterAll throw — mask test failure original
Khi afterAll throw, Playwright log error nhưng không fail test. Nếu test đã fail và cleanup cũng fail, output chỉ thấy test failure — cleanup error có thể bị bỏ qua trong CI. Cần đọc kỹ full log để không miss.
// SAI — throw từ cleanup làm log bị noise
test.afterAll(async () => {
await dropSchema(pool, schemaName); // throw nếu schema không tồn tại
});
// ĐÚNG — bắt và log
test.afterAll(async () => {
await dropSchema(pool, schemaName).catch(err =>
console.error('[afterAll] dropSchema failed:', err.message)
);
});
Pitfall 2: Null reference khi beforeAll fail
// SAI — pool chưa được assign nếu createPool() throw
let pool: Pool;
test.beforeAll(async () => {
pool = await createPool(); // fail ở đây
await migrateSchema(pool);
});
test.afterAll(async () => {
await pool.end(); // TypeError: Cannot read properties of undefined
});
// ĐÚNG — optional chain
test.afterAll(async () => {
await pool?.end(); // an toàn kể cả khi pool undefined
});
Pitfall 3: Quên LIFO khi multiple afterAll phụ thuộc nhau
// SAI — pool.end() chạy TRƯỚC dropSchema (LIFO order)
// dropSchema cần pool còn mở nhưng pool đã end rồi
test.afterAll(async () => {
await pool?.end(); // khai báo trước → chạy SAU (LIFO)
});
test.afterAll(async () => {
await dropSchema(pool, schemaName); // khai báo sau → chạy TRƯỚC
// pool đã end ở đây → error
});
// ĐÚNG — đảo thứ tự khai báo
test.afterAll(async () => {
await dropSchema(pool, schemaName); // khai báo trước → chạy SAU
// pool vẫn mở khi chạy dòng này → OK
});
test.afterAll(async () => {
await pool?.end(); // khai báo sau → chạy TRƯỚC (LIFO)
});
Pitfall 4: Cleanup chậm vượt timeout — partial state
// SAI — cleanup drop schema lớn có thể mất >30s
test.afterAll(async () => {
await db.query('DROP SCHEMA test_schema CASCADE');
// Nếu schema có nhiều table + data → timeout 30s bị vượt
// Error: afterAll hook timeout of 30000ms exceeded
// Schema bị drop dở, state partial → run sau có thể nhận orphan table
});
// ĐÚNG — set timeout đủ lớn hoặc tách cleanup nặng
test.afterAll(async () => {
test.setTimeout(120_000); // 2 phút cho cleanup nặng
await db.query('DROP SCHEMA test_schema CASCADE');
});
Tổng Kết
afterAllchạy 1 lần sau test cuối trong scope — kể cả khi test fail và kể cả khibeforeAllfail.- Chỉ worker-scope fixture (
browser,playwright, custom worker fixture) khả dụng.pagevàcontextkhông dùng được. - Cleanup phải idempotent: dùng optional chain
?., flag trạng thái,IF EXISTStrong SQL,try/catchper step. - Nhiều
afterAlltrong cùng scope chạy LIFO — khai báo sau chạy trước. Thiết kế thứ tự khai báo cho đúng khi cleanup có dependency. afterAllthrow không fail test đã chạy — chỉ log error. Cleanup failure có thể mask hoặc bị miss trong CI.- Timeout mặc định = test timeout (thường 30s). Override bằng
test.setTimeout(N)bên trong hook khi cleanup chậm. - Worker fixture cleanup chạy SAU
afterAllcuối cùng — có thể inject worker fixture vàoafterAllnếu cần. afterAllkhácglobalTeardown: scope per file/describe vs toàn run, có fixture vs không có fixture.- Cleanup không guaranteed khi process bị kill — cần external cleanup strategy cho resource persistent.
Quiz
Câu 1
File có 4 test, test số 2 fail với expect() không match. afterAll có được gọi không? Kết quả test thay đổi như thế nào?
Đáp án
Có — afterAll luôn chạy sau test cuối của scope, bất kể test nào fail. Test số 2 vẫn báo FAIL trong kết quả; afterAll chạy sau test số 4 hoàn thành. Kết quả tổng: 3 PASSED, 1 FAILED.
Câu 2
Đoạn code dưới đây có vấn đề gì? Sửa thế nào?
let server: http.Server;
test.beforeAll(async () => {
server = http.createServer(handler);
server.listen(3099); // listen không await — có thể chưa sẵn sàng
});
test.afterAll(async () => {
server.close(); // close không await
});
Đáp án
Hai vấn đề:
server.listen(3099)không được await — test có thể chạy trước khi server thực sự ready. Sửa: wrap trong Promise và resolve trong callback.server.close()không được await —afterAllkết thúc trước khi server thực sự đóng. Sửa: wrap trong Promise với callback.
test.beforeAll(async () => {
server = http.createServer(handler);
await new Promise<void>(resolve => server.listen(3099, resolve));
});
test.afterAll(async () => {
await new Promise<void>((resolve, reject) => {
server?.close(err => err ? reject(err) : resolve());
});
});
Câu 3
File khai báo 3 afterAll theo thứ tự A, B, C. Thứ tự chạy thực tế là gì?
Đáp án
C → B → A. Playwright thực thi nhiều afterAll trong cùng scope theo LIFO (Last In, First Out). Hook khai báo cuối cùng (C) chạy đầu tiên.
Câu 4
beforeAll tạo DB schema rồi fail khi seed data. Biến schemaName đã được assign trước khi fail. Viết afterAll cleanup đúng cách.
Đáp án
let schemaName: string | null = null;
let pool: Pool | null = null;
test.beforeAll(async () => {
pool = new Pool({ connectionString: process.env.DATABASE_URL });
schemaName = `test_${Date.now()}`;
await pool.query(`CREATE SCHEMA ${schemaName}`);
await seedData(pool, schemaName); // fail ở đây
});
test.afterAll(async () => {
// Schema đã tạo → cần drop
if (schemaName !== null && pool !== null) {
await pool.query(`DROP SCHEMA IF EXISTS ${schemaName} CASCADE`)
.catch(err => console.error('[afterAll] dropSchema:', err.message));
}
await pool?.end().catch(err =>
console.error('[afterAll] pool.end:', err.message)
);
});
Câu 5
Khi nào nên dùng afterAll thay vì worker-scope fixture teardown (code sau await use())?
Đáp án
Dùng afterAll khi:
- Cleanup logic đặc thù cho một file/describe cụ thể, không reuse qua nhiều file.
- Cleanup dùng biến module-level tạo trong
beforeAll(không qua fixture injection). - Muốn cleanup chạy trong scope nhỏ hơn — một describe thay vì toàn worker.
Dùng worker fixture teardown khi:
- Cleanup gắn với resource được inject vào nhiều file trong cùng worker.
- Muốn type-safe injection và reuse pattern qua nhiều test file.
Bài Tiếp Theo
Bài 35 chuyển sang beforeEach và afterEach — hooks chạy per test, có quyền truy cập testInfo để đọc kết quả test và metadata.
