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ẽ:
- Khai báo
globalTeardowntrong config đúng cú pháp và hiểu khi nào nó được gọi. - Phân biệt 2 cách viết teardown: file riêng và return function từ
globalSetup. - Share state từ setup xuống teardown qua closure và
process.env. - Hiểu các limitation của
globalTeardownlegacy so với teardown project. - Tránh được 4 pitfall phổ biến khiến teardown âm thầm không chạy hoặc làm hỏng CI.
globalTeardown Là Gì
globalTeardown là field trong playwright.config.ts nhận đường dẫn đến một module TypeScript/JavaScript export default một async function. Function đó được Playwright gọi đúng 1 lần sau khi toàn bộ test run hoàn thành — bao gồm tất cả worker và tất cả project.
Đây là cơ chế đối xứng với globalSetup: nếu globalSetup khởi động resource trước run thì globalTeardown dọn dẹp resource sau run.
Ví dụ use case điển hình:
- Xoá test database schema sau toàn run.
- Kill mock server process được spawn trong
globalSetup. - Giải phóng connection pool hoặc release distributed lock.
- Gửi notification với kết quả run (webhook, Slack).
- Xoá temp file và artifact tạm thời.
Điểm cần phân biệt ngay: globalTeardown không phải hook afterAll. afterAll chạy sau từng describe block trong một worker — globalTeardown chạy sau khi mọi worker của toàn run đã xong.
Cú Pháp Khai Báo
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
globalSetup: require.resolve('./global-setup'),
globalTeardown: require.resolve('./global-teardown'),
use: {
baseURL: 'http://localhost:3000',
},
});
Một số điểm về cú pháp:
require.resolve(): trả về absolute path của module — tránh lỗi path resolution khi chạy từ thư mục khác.- Không bắt buộc khai báo cả hai: có thể có
globalTeardownmà không cầnglobalSetup, và ngược lại. - Đường dẫn tương đối cũng hoạt động nhưng
require.resolve()là cách được khuyến nghị.
Anatomy — File globalTeardown
File teardown export default một async function nhận tham số FullConfig:
// global-teardown.ts
import { FullConfig } from '@playwright/test';
async function globalTeardown(config: FullConfig) {
// config chứa toàn bộ resolved playwright config
// Có thể đọc: config.projects, config.use, config.outputDir, v.v.
await dropTestDatabase();
await stopMockServer();
}
export default globalTeardown;
Đây là một Node.js function thuần — không có fixture Playwright. Không thể dùng page, request, hay bất kỳ fixture nào ở đây. Nếu cần fixture (ví dụ gọi API cleanup), teardown project (bài 99) là lựa chọn phù hợp hơn.
Tham số FullConfig chứa config đã được resolved — tức là các giá trị đã merge với default, với use từ project level và root level. Trong hầu hết cleanup, tham số này không cần dùng đến, nhưng hữu ích khi cần đọc config.outputDir để biết nơi Playwright lưu artifact.
Behavior Chi Tiết
Behavior quan trọng cần nắm trước khi viết teardown:
| Tình huống | globalTeardown có chạy không? |
|---|---|
| Tất cả test pass | Có |
| Một số test fail | Có — teardown chạy kể cả khi có test fail |
| Tất cả test fail | Có |
globalSetup throw |
Không — setup fail → Playwright abort → teardown bị skip |
| Process nhận SIGKILL | Không — SIGKILL bypass mọi graceful shutdown |
| Process nhận SIGTERM | Có — Playwright bắt SIGTERM và chạy teardown trước khi exit |
Điểm đặc biệt quan trọng: nếu globalSetup throw, globalTeardown không được gọi. Lý do: Playwright coi setup fail là trạng thái chưa có gì được khởi tạo — teardown lúc này không có gì để dọn. Tuy nhiên điều này có một hệ quả: nếu setup khởi tạo được một phần rồi mới fail, phần đã khởi tạo sẽ không được cleanup. Bài học: trong globalSetup, nếu khởi tạo resource thành công thì nên cleanup ngay trong try/catch của chính setup, không trông chờ vào teardown.
2 Cách Định Nghĩa Teardown
Cách 1: File Riêng (globalTeardown config)
Khai báo 2 field độc lập trong config:
// playwright.config.ts
export default defineConfig({
globalSetup: require.resolve('./global-setup'),
globalTeardown: require.resolve('./global-teardown'),
});
// global-teardown.ts
import { FullConfig } from '@playwright/test';
async function globalTeardown(config: FullConfig) {
await dropTestDatabase();
await stopMockServer();
}
export default globalTeardown;
Phù hợp khi setup và teardown là 2 logic tách biệt hoàn toàn, không cần chia sẻ state.
Cách 2: Return Function Từ globalSetup
Khi globalSetup return một async function, Playwright tự động dùng function đó làm teardown — không cần khai báo globalTeardown trong config:
// global-setup.ts
import { FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
const server = await startMockServer();
const db = await createTestDatabase();
// Return function → đây chính là teardown
return async () => {
await db.drop();
await server.close();
};
}
export default globalSetup;
Playwright sẽ gọi function được return này sau khi toàn run kết thúc, đúng như behavior của globalTeardown.
Khi nào dùng cách nào:
| Tiêu chí | File riêng | Return function |
|---|---|---|
| Share state setup → teardown | Khó — phải dùng process.env hoặc file |
Dễ — access trực tiếp qua closure |
| Tách biệt concern | Tốt — 2 file riêng biệt, dễ đọc độc lập | Kém hơn — teardown logic nằm trong setup file |
| Cleanup resource (server, connection) | Phải serialize state (PID, port) ra env | Tốt nhất — giữ reference object trực tiếp |
| Teardown độc lập (không cần setup) | Có thể dùng, không cần globalSetup | Không áp dụng được |
Share State: Setup → Teardown
Setup và teardown chạy trong cùng một Node.js process nhưng không chia sẻ module scope — teardown file là một module riêng. Có 2 cách để share state:
Cách A: Return Function (Closure)
Đây là cách sạch nhất khi cần giữ reference đến object JavaScript (server instance, connection, v.v.):
// global-setup.ts
async function globalSetup() {
const server = await startMockServer({ port: 4000 });
// server là object — không serialize được sang env
return async () => {
// Closure: truy cập trực tiếp biến server từ scope bên ngoài
await server.close();
console.log('Mock server stopped');
};
}
export default globalSetup;
Cách B: process.env (File Riêng)
Khi dùng 2 file riêng, state phải được serialize thành string và truyền qua process.env:
// global-setup.ts
import { FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
const server = await startMockServer({ port: 4000 });
// Lưu PID để teardown có thể kill process
process.env.MOCK_SERVER_PID = String(server.pid);
// Lưu port để test biết kết nối đến đâu
process.env.MOCK_SERVER_PORT = '4000';
}
export default globalSetup;
// global-teardown.ts
import { FullConfig } from '@playwright/test';
async function globalTeardown(config: FullConfig) {
const pid = process.env.MOCK_SERVER_PID;
if (pid) {
try {
process.kill(Number(pid));
} catch {
// Process đã không còn tồn tại — bỏ qua
}
}
}
export default globalTeardown;
Lưu ý quan trọng về process.env: các worker test chạy trong process riêng, nhưng globalSetup và globalTeardown chạy trong process chính (orchestrator). Giá trị set trong globalSetup vào process.env sẽ vẫn còn đó khi globalTeardown chạy trong cùng process đó.
Hạn chế của cách này: chỉ truyền được string — không truyền được object, function, hoặc stream. Với resource phức tạp hơn, nên dùng return function (closure).
Use Case Thực Tế
Drop Test Database
// global-teardown.ts
import { FullConfig } from '@playwright/test';
import { Pool } from 'pg';
async function globalTeardown(config: FullConfig) {
const pool = new Pool({ connectionString: process.env.TEST_DATABASE_URL });
try {
// Xoá toàn bộ data test (truncate, không drop schema)
await pool.query('TRUNCATE TABLE users, orders, products RESTART IDENTITY CASCADE');
} finally {
await pool.end();
}
}
export default globalTeardown;
Delete Temp Files và Artifact
// global-teardown.ts
import { FullConfig } from '@playwright/test';
import { rm, access } from 'fs/promises';
import path from 'path';
async function globalTeardown(config: FullConfig) {
const uploadDir = path.join(process.cwd(), 'test-uploads');
try {
await access(uploadDir); // kiểm tra tồn tại trước khi xoá
await rm(uploadDir, { recursive: true, force: true });
} catch {
// Thư mục không tồn tại — bỏ qua
}
}
export default globalTeardown;
Send Run Notification
// global-teardown.ts
import { FullConfig } from '@playwright/test';
async function globalTeardown(config: FullConfig) {
const passed = Number(process.env.TEST_PASSED_COUNT ?? 0);
const failed = Number(process.env.TEST_FAILED_COUNT ?? 0);
const webhookUrl = process.env.SLACK_WEBHOOK_URL;
if (!webhookUrl) return;
await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `Test run xong: ${passed} passed, ${failed} failed`,
}),
});
}
export default globalTeardown;
Lưu ý: process.env.TEST_PASSED_COUNT / TEST_FAILED_COUNT phải được set bởi logic khác (ví dụ custom reporter) — globalTeardown không tự nhận kết quả test.
Idempotent Cleanup
Idempotent cleanup nghĩa là: chạy teardown nhiều lần cho ra kết quả giống như chạy 1 lần. Điều này đặc biệt quan trọng vì teardown có thể bị chạy lại (retry CI, chạy local sau khi CI đã cleanup, v.v.).
Ví dụ không idempotent — dễ fail khi chạy lần 2:
// KHÔNG tốt: fail nếu server đã bị kill hoặc DB không tồn tại
async function globalTeardown(config: FullConfig) {
process.kill(Number(process.env.MOCK_SERVER_PID)); // throw nếu PID không tồn tại
await db.dropDatabase('test_db'); // throw nếu DB không tồn tại
}
Ví dụ idempotent — an toàn khi chạy lại:
// Tốt: xử lý gracefully khi resource đã không còn tồn tại
async function globalTeardown(config: FullConfig) {
// Kill process — bỏ qua nếu không còn chạy
const pid = process.env.MOCK_SERVER_PID;
if (pid) {
try {
process.kill(Number(pid));
} catch {
// ESRCH: no such process — đã bị kill trước đó, OK
}
}
// Drop DB — bỏ qua nếu không tồn tại
try {
await db.query('DROP DATABASE IF EXISTS test_db');
} catch (error) {
console.error('DB cleanup failed:', error);
// Không re-throw: tránh làm CI fail chỉ vì cleanup
}
}
Quy tắc đơn giản: mỗi cleanup operation nên dùng cú pháp "IF EXISTS" hoặc wrap trong try/catch. Nhưng hãy log lỗi trước khi nuốt — silent failure làm khó debug.
globalTeardown Fail — Hệ Quả
Nếu globalTeardown throw (unhandled error), Playwright sẽ exit với non-zero exit code — dù tất cả test đã pass.
Trên CI, điều này có nghĩa: pipeline báo FAILED dù kết quả test thật ra là PASSED. Build đỏ vì cleanup fail, không phải vì test fail. Đây là nguồn gốc của nhiều incident CI khó debug.
Hậu quả cụ thể:
- GitHub Actions step exit non-zero → job fail → pipeline fail.
- Nếu pipeline dựa vào exit code để quyết định deploy, deploy bị block dù test pass.
- Report (nếu dùng HTML reporter) có thể không được publish vì process exit non-zero trước khi flush.
Khuyến nghị: wrap mọi cleanup operation trong try/catch riêng biệt. Chỉ re-throw khi thật sự muốn CI biết rằng có vấn đề với cleanup (ví dụ cleanup fail là dấu hiệu môi trường bị hỏng). Trong hầu hết trường hợp, log và bỏ qua là đủ.
async function globalTeardown(config: FullConfig) {
// Tách biệt từng operation — fail 1 không làm fail cái còn lại
try {
await dropTestDatabase();
} catch (error) {
console.error('[teardown] DB cleanup failed:', error);
}
try {
await stopMockServer();
} catch (error) {
console.error('[teardown] Mock server stop failed:', error);
}
}
Limitation Legacy
globalTeardown là API legacy — vẫn được support nhưng có những giới hạn rõ ràng so với teardown project (bài 99):
| Limitation | Chi tiết |
|---|---|
| Không hiển thị trong reporter | Kết quả teardown không xuất hiện trong HTML/JUnit report — khó biết teardown pass hay fail |
| Không có fixture Playwright | Không thể dùng page, request, hay custom fixture — chỉ là Node.js thuần |
| Không retry | Nếu teardown fail, không có cơ chế retry tự động |
| SIGKILL không chạy | Khi CI kill process bằng SIGKILL (ví dụ timeout), teardown không được gọi — resource leak |
| Không gắn được với project cụ thể | Một globalTeardown cho toàn bộ run — không thể có teardown khác nhau per project |
| Share state khó | Chỉ qua process.env (string) hoặc return function — không có cơ chế native nào khác |
Playwright khuyến nghị dùng teardown project cho các use case mới. globalTeardown phù hợp khi: cần cleanup đơn giản (kill process, xoá file), không cần fixture, và không quan tâm đến visibility trong reporter.
Pitfall Thường Gặp
Pitfall 1: globalTeardown Throw Làm CI Fail Dù Test Pass
Teardown throw unhandled error → Playwright exit non-zero → CI báo fail. Tất cả test đã xanh nhưng pipeline đỏ. Fix: wrap mọi operation trong try/catch, chỉ re-throw nếu có lý do cụ thể.
Pitfall 2: State Từ globalSetup Không Share Được Sang File Riêng
Khi dùng 2 file riêng (global-setup.ts và global-teardown.ts), không có shared module scope — biến const server = ... trong setup hoàn toàn không accessible trong teardown file. Phải dùng process.env hoặc chuyển sang return function pattern.
// global-setup.ts — LỖI NÀY SẼ KHÔNG WORK
let serverInstance: Server; // Biến này chỉ sống trong module này
async function globalSetup() {
serverInstance = await startServer();
}
// global-teardown.ts — không thể access serverInstance từ setup file
async function globalTeardown() {
await serverInstance.close(); // ReferenceError hoặc undefined
}
Pitfall 3: SIGKILL Không Trigger Teardown
Khi CI runner timeout và kill process bằng SIGKILL (không phải SIGTERM), globalTeardown không được gọi. Resource tạo ra trong setup (DB rows, temp files, server process) không được cleanup. Fix: thiết kế resource có TTL hoặc thêm cleanup script chạy độc lập sau CI.
Pitfall 4: Cleanup Phụ Thuộc State Chưa Được Init (Setup Fail)
Khi globalSetup fail, globalTeardown không được gọi. Nếu setup đã tạo được resource trước khi fail thì resource đó sẽ không được cleanup. Ví dụ: setup tạo DB xong, sau đó start server fail → DB đã tồn tại nhưng teardown không chạy → DB leak.
Fix: trong globalSetup, dùng pattern rollback khi fail:
async function globalSetup() {
let db: Database | null = null;
try {
db = await createTestDatabase(); // step 1 thành công
await startMockServer(); // step 2 fail
} catch (error) {
// Rollback những gì đã tạo được
if (db) await db.drop().catch(() => {});
throw error; // re-throw để Playwright biết setup fail
}
}
Quiz
Câu 1: globalTeardown có chạy không nếu 50% test trong run bị fail?
Đáp án
Có. globalTeardown chạy sau khi toàn run kết thúc, bất kể test pass hay fail. Chỉ khi globalSetup throw hoặc process bị SIGKILL thì teardown mới không chạy.
Câu 2: globalSetup khởi tạo xong một server object, sau đó throw lỗi khi kết nối DB. globalTeardown có được gọi không? Server có được close không?
Đáp án
globalTeardown không được gọi vì setup đã throw. Server không được close trừ khi setup tự cleanup trong catch block của mình. Đây là lý do cần pattern rollback trong setup: cleanup những gì đã tạo được trước khi re-throw.
Câu 3: Bạn muốn close một server object sau toàn run. Nên dùng return function hay file riêng? Tại sao?
Đáp án
Return function. Vì server object không serialize được sang string — không thể truyền qua process.env. Return function giữ reference trực tiếp đến object qua closure, cho phép gọi server.close() trong teardown mà không cần serialize.
Câu 4: Khai báo globalTeardown trong config nhưng không khai báo globalSetup. Điều này có hợp lệ không?
Đáp án
Hợp lệ. globalSetup và globalTeardown là 2 field độc lập — có thể dùng cả hai, chỉ một, hoặc không dùng cái nào.
Câu 5: globalTeardown throw một error. Test run đã pass toàn bộ. CI exit code là bao nhiêu?
Đáp án
Non-zero (fail). Playwright coi globalTeardown throw là lỗi nghiêm trọng và exit với non-zero code — dù toàn bộ test đã pass. Pipeline sẽ báo fail.
Bài Tiếp Theo
Bài 99: Setup Project vs globalSetup — Chọn Cái Nào — so sánh 2 cơ chế setup theo từng tiêu chí: fixture access, visibility trong reporter, scope per project hay toàn run, và hướng dẫn khi nào nên dùng cái nào.
