Mục lục
- Mục Tiêu Bài Học
- teardown Là Gì
- Cú Pháp Khai Báo
- Teardown File — Anatomy
- Flow Thực Thi Chi Tiết
- So Sánh Với afterAll
- So Sánh Với globalTeardown Legacy
- Multi-project Teardown
- Combine Với dependencies — Full Lifecycle
- Use Case Thực Tế
- Idempotent Cleanup
- Limitation
- Pitfall Thường Gặp
- Quiz
- 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ẽ:
- Khai báo
teardownproject trongplaywright.config.tsđúng cú pháp. - Viết teardown file với import
test as teardownvà nhiều cleanup action. - Nắm rõ khi nào teardown chạy, khi nào bị skip.
- Phân biệt teardown project với
afterAllhook vàglobalTeardownlegacy. - Áp dụng idempotent cleanup để teardown fail không làm hỏng CI exit code.
teardown Là Gì
Field teardown trong object project nhận tên của một project khác. Project được chỉ định đó sẽ chạy sau khi project khai báo nó kết thúc — bất kể kết quả pass hay fail.
Teardown project là một project Playwright bình thường — nó có testMatch để scope file, có thể có use options, và được hiển thị trong HTML reporter với kết quả riêng. Sự khác biệt duy nhất là cách Playwright lên lịch chạy nó: không phải do user gọi trực tiếp, mà do lifecycle của project khác trigger.
Ví dụ thực tế: project main chạy toàn bộ test E2E, tạo ra test data trong DB và file upload trên server. Sau khi main xong, Playwright tự động chạy project cleanup để xoá toàn bộ dữ liệu đó — không cần script wrapper bên ngoài.
Cú Pháp Khai Báo
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
// Project teardown — khai báo trước để dễ đọc config
{
name: 'cleanup',
testMatch: /cleanup\.teardown\.ts/,
},
// Project chính — chỉ định teardown bằng tên project cleanup
{
name: 'main',
teardown: 'cleanup',
use: { ...devices['Desktop Chrome'] },
},
],
});
Một số điểm cần lưu ý về cú pháp:
- Thứ tự khai báo trong array không quan trọng: Playwright resolve lifecycle theo
teardownfield, không phải thứ tự array. Khai báocleanuptrước hay saumainđều hoạt động như nhau. - Giá trị của
teardownphải khớp chính xác tên project: là exact match, không phải glob. Viết sai tên sẽ bị Playwright báo lỗi khi validate config. - Teardown project nên có
testMatch: để tách biệt file teardown khỏi các test thông thường. Convention phổ biến là dùng đuôi.teardown.ts.
Teardown File — Anatomy
File teardown dùng cú pháp test Playwright bình thường, nhưng import alias test as teardown để phân biệt với test spec thông thường trong cùng codebase:
// cleanup.teardown.ts
import { test as teardown } from '@playwright/test';
import { rm } from 'fs/promises';
// Xoá thư mục upload sau test
teardown('delete uploaded files', async () => {
await rm('uploads/', { recursive: true, force: true });
});
// Gọi API cleanup để xoá test data trong DB
teardown('truncate test data', async ({ request }) => {
await request.post('/api/test/cleanup');
});
// Nhiều teardown action trong 1 file — chạy tuần tự theo thứ tự khai báo
teardown('clear redis cache', async ({ request }) => {
await request.post('/api/test/cache/flush');
});
Alias teardown không bắt buộc về mặt kỹ thuật — dùng tên test thông thường cũng chạy được. Nhưng alias giúp đồng nghiệp đọc code nhận ra ngay đây là teardown logic, không phải test case thật.
Teardown file có đầy đủ fixture access — bao gồm request (APIRequestContext), page, và custom fixture. Không cần viết logic network thủ công.
Flow Thực Thi Chi Tiết
Flow cơ bản khi config có 1 main project và 1 teardown project:
- Playwright chạy toàn bộ test trong project
main. - Khi
mainkết thúc (dù pass hay fail), Playwright tự động lên lịch chạy projectcleanup. - Project
cleanupchạy trong separate process — không chia sẻ state hay worker vớimain. - Kết quả của
cleanupđược ghi nhận độc lập trong reporter.
Quan trọng — teardown và trường hợp setup fail: Behavior này phụ thuộc vào kết hợp dependencies + teardown. Khi không có setup dependency:
- Main pass → teardown chạy.
- Main fail (một số test fail) → teardown vẫn chạy.
- Main bị interrupt (thoát giữa chừng) → teardown vẫn được trigger nếu Playwright còn control flow.
Khi kết hợp với setup dependency (xem mục 9): nếu setup fail → main bị skip → teardown cũng bị skip theo (không có gì để cleanup vì main chưa chạy). Đây là behavior có chủ ý — tránh cleanup state chưa từng được tạo ra.
Trường hợp SIGKILL (process bị kill cứng, ví dụ CI timeout giết process): teardown không được trigger. Chỉ SIGTERM (graceful shutdown) mới đảm bảo teardown chạy.
So Sánh Với afterAll
| Tiêu chí | afterAll hook |
Teardown project |
|---|---|---|
| Scope | Một describe block hoặc một file test |
Toàn bộ project — sau khi mọi file, mọi worker kết thúc |
| Process | Cùng worker process với test | Separate process độc lập |
| Hiển thị trong reporter | Không có entry riêng trong HTML reporter | Có project entry riêng, hiện kết quả từng teardown step |
| Khi test fail | Chạy nếu cùng describe/worker không bị interrupt | Luôn chạy sau khi main project kết thúc |
| Fixture access | Đầy đủ — cùng scope test | Đầy đủ — fresh context mới |
| Dùng khi | Cleanup nhỏ trong 1 file: đóng connection, xoá 1 bản ghi cụ thể | Cleanup toàn cục sau cả run: clear DB, clear S3 bucket, notify Slack |
afterAll và teardown project không loại trừ nhau — có thể dùng cả hai cùng lúc. afterAll cleanup local state trong từng file, còn teardown project cleanup global state sau toàn run.
So Sánh Với globalTeardown Legacy
| Tiêu chí | globalTeardown |
Teardown project |
|---|---|---|
| Khai báo | Field globalTeardown ở root config — trỏ đến 1 file JS/TS export default function |
Field teardown trong project object — trỏ đến tên project |
| Scope | 1 function global cho toàn bộ run | 1 project đầy đủ — có thể nhiều test case cleanup riêng biệt |
| Gắn với project nào | Không gắn với project nào — chạy sau TẤT CẢ project | Gắn với project cụ thể — chạy sau đúng project đó |
| Hiện trong reporter | Không — invisible với reporter | Có — entry đầy đủ trong HTML/JUnit report |
| Fixture access | Không có fixture Playwright — chỉ là Node.js function | Có đầy đủ: request, page, custom fixture |
| Multi-project support | Một globalTeardown cho tất cả — khó gắn cleanup với từng project | Mỗi project có teardown riêng — tường minh và dễ maintain |
Playwright khuyến nghị ưu tiên teardown project hơn globalTeardown cho các use case mới. globalTeardown vẫn được support nhưng ít flexible hơn — không thể dùng fixture và không hiển thị trong report.
Multi-project Teardown
Mỗi project chỉ có thể trỏ đến 1 teardown project. Khi có nhiều main project cần cleanup riêng, khai báo teardown project riêng cho từng cái:
// playwright.config.ts
export default defineConfig({
projects: [
// Teardown projects
{
name: 'cleanup-A',
testMatch: /cleanup-a\.teardown\.ts/,
},
{
name: 'cleanup-B',
testMatch: /cleanup-b\.teardown\.ts/,
},
// Main projects
{
name: 'project-A',
teardown: 'cleanup-A',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'project-B',
teardown: 'cleanup-B',
use: { ...devices['Desktop Firefox'] },
},
],
});
Trong ví dụ này:
project-Avàproject-Bchạy song song (không có dependency).- Khi
project-Axong →cleanup-Ađược trigger. - Khi
project-Bxong →cleanup-Bđược trigger. cleanup-Avàcleanup-Bcó thể chạy đồng thời nếu worker pool còn slot.
Nếu nhiều main project cần cùng cleanup logic, cách đơn giản nhất là trỏ cả hai về cùng một teardown project:
projects: [
{ name: 'cleanup', testMatch: /global\.teardown\.ts/ },
{ name: 'project-A', teardown: 'cleanup', use: { ...devices['Desktop Chrome'] } },
{ name: 'project-B', teardown: 'cleanup', use: { ...devices['Desktop Firefox'] } },
],
Khi cả project-A và project-B đều cần cleanup, Playwright chạy cleanup một lần sau khi cả hai kết thúc — không chạy hai lần riêng biệt. Đây là behavior quan trọng cần nhớ để tránh tình huống cleanup chạy nhiều lần không mong muốn.
Combine Với dependencies — Full Lifecycle
Kết hợp dependencies và teardown để có vòng đời đầy đủ: setup → test → cleanup. Đây là pattern chuẩn cho test suite cần chuẩn bị và dọn dẹp state:
// playwright.config.ts
export default defineConfig({
projects: [
// Phase 0 — setup
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
// Phase 2 — cleanup (khai báo để main có thể tham chiếu)
{
name: 'cleanup',
testMatch: /.*\.teardown\.ts/,
},
// Phase 1 — main test
{
name: 'main',
dependencies: ['setup'], // chờ setup xong mới chạy
teardown: 'cleanup', // sau khi xong thì chạy cleanup
use: { ...devices['Desktop Chrome'] },
},
],
});
Thứ tự thực thi đảm bảo: setup → main → cleanup.
Behavior chi tiết theo từng kịch bản:
| Kịch bản | Setup | Main | Cleanup |
|---|---|---|---|
| Happy path | Pass | Pass | Chạy |
| Test fail | Pass | Có test fail | Chạy (dù main có fail) |
| Setup fail | Fail | Skip | Skip (main chưa chạy nên không có gì để cleanup) |
Behavior "setup fail → cleanup skip" là có chủ ý: nếu setup chưa tạo ra bất kỳ state nào (vì fail sớm), chạy cleanup là vô nghĩa và có thể gây lỗi (cleanup thứ không tồn tại).
Use Case Thực Tế
Delete test data
Test E2E thường tạo user, order, product trong DB. Sau khi chạy xong, cleanup xoá tất cả bản ghi có prefix test:
teardown('delete test users', async ({ request }) => {
await request.delete('/api/test/users?prefix=test_');
});
teardown('delete test orders', async ({ request }) => {
await request.delete('/api/test/orders?created_by=test_runner');
});
Reset DB schema
Với test database riêng, có thể truncate hoặc drop-recreate toàn bộ schema:
teardown('truncate test db', async ({ request }) => {
// Endpoint này chỉ tồn tại ở test environment
await request.post('/api/test/db/truncate', {
data: { tables: ['users', 'orders', 'uploads'] },
});
});
Delete uploaded files
Test upload file tạo file rác trên disk hoặc S3:
import { test as teardown } from '@playwright/test';
import { rm } from 'fs/promises';
teardown('delete local uploads', async () => {
// Xoá thư mục upload local của test server
await rm('test-server/uploads/', { recursive: true, force: true });
});
teardown('clear S3 test bucket', async ({ request }) => {
await request.post('/api/test/s3/clear', {
data: { bucket: 'my-app-test-uploads' },
});
});
Send notification
Gửi kết quả test lên Slack hoặc Telegram sau khi run xong:
teardown('notify slack', async ({ request }) => {
const status = process.env.TEST_STATUS ?? 'unknown';
await request.post(process.env.SLACK_WEBHOOK!, {
data: {
text: `E2E run completed: ${status}`,
},
});
});
Generate aggregated report
Gọi script tổng hợp metrics từ nhiều nguồn sau khi test xong:
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
teardown('aggregate metrics', async () => {
await execAsync('node scripts/aggregate-test-metrics.js');
});
Idempotent Cleanup
Cleanup logic nên idempotent — chạy nhiều lần không gây hại, và nếu fail thì không ảnh hưởng CI exit code. Vấn đề xảy ra khi cleanup bị lỗi mà main test đã pass: CI sẽ báo fail vì teardown project fail, dù business logic đã OK.
Pattern phổ biến: wrap cleanup trong try/catch và log warning thay vì rethrow:
import { test as teardown } from '@playwright/test';
teardown('cleanup test data', async ({ request }) => {
try {
await request.delete('/api/test/data');
} catch (err) {
// Log nhưng không throw — cleanup fail không block CI
console.warn('Cleanup failed (non-fatal):', err);
}
});
teardown('clear uploads', async () => {
try {
const { rm } = await import('fs/promises');
await rm('uploads/', { recursive: true, force: true });
} catch (err) {
console.warn('Upload cleanup failed:', err);
}
});
Hai trường hợp cần cân nhắc:
- Cleanup không quan trọng (ví dụ: xoá file log tạm): luôn dùng try/catch, không throw.
- Cleanup quan trọng (ví dụ: xoá test user để tránh ảnh hưởng production): để throw, nhưng cần có monitoring riêng để alert khi cleanup fail.
Dùng force: true trong Node.js rm() để không throw khi path không tồn tại — tránh lỗi "file not found" trên các run lần đầu:
// Không throw kể cả khi thư mục chưa tồn tại
await rm('uploads/', { recursive: true, force: true });
Limitation
- 1 teardown per project: mỗi project chỉ có thể khai báo 1
teardown. Nếu cần nhiều nhóm cleanup logic riêng biệt, gộp tất cả vào 1 teardown file với nhiềuteardown()call — không thể point đến nhiều teardown project cùng lúc. - Teardown không nhận kết quả của main: teardown chạy trong separate process và không có access thông tin pass/fail count của main project. Cleanup là "blind" — không biết bao nhiêu test đã fail hay fail ở đâu. Nếu cần conditional cleanup dựa trên kết quả, phải dùng custom reporter để ghi kết quả ra file rồi teardown đọc lại.
- SIGKILL bỏ qua teardown: CI timeout dùng SIGKILL (không phải SIGTERM) sẽ kill process ngay lập tức mà không trigger teardown. Nếu cleanup quan trọng, cần đảm bảo CI dùng SIGTERM với grace period đủ dài.
- Teardown bị skip khi dùng --no-deps: flag
--no-deps(bỏ qua dependencies) cũng bỏ qua teardown. Cần lưu ý khi debug local.
Pitfall Thường Gặp
Pitfall 1 — Teardown fail làm CI fail dù main pass
Teardown là project thật — nếu teardown throw uncaught error, CI exit code sẽ là non-zero dù toàn bộ main test đã pass. Kết quả CI fail dù business logic đúng. Cách tránh: wrap cleanup logic trong try/catch và log warning thay vì throw (xem mục 11).
Pitfall 2 — Teardown phụ thuộc vào state mà main đã clear
Ví dụ: teardown cần đọc list ID của các bản ghi đã tạo trong test để xoá đúng bản ghi đó. Nhưng bản ghi đó đã bị test trong afterEach xoá rồi. Teardown chạy sau toàn bộ afterEach — nếu state cần thiết đã bị clear trước, teardown sẽ không cleanup được gì. Giải pháp: dùng endpoint cleanup tập trung (truncate theo prefix, không cần biết ID cụ thể).
Pitfall 3 — Quên gitignore artifact của teardown
Teardown tạo ra file log, report tổng hợp, hoặc temp files. Nếu không thêm vào .gitignore, chúng sẽ bị commit vào repo qua lần commit vô tình sau khi chạy test local:
# .gitignore
test-results/
playwright-report/
uploads/ # thư mục upload test
*.teardown-report.json
Pitfall 4 — Teardown chạy chậm làm tăng CI duration đáng kể
Nếu teardown gọi nhiều API cleanup tuần tự (nhiều teardown() call chạy một sau một), thời gian tích lũy có thể lớn. Ví dụ: 10 API call × 500ms mỗi call = 5 giây thêm vào mỗi run. Với CI chạy nhiều lần mỗi ngày, con số này đáng kể. Cách tối ưu: song song hóa cleanup với Promise.all trong 1 teardown step, hoặc dùng bulk delete endpoint thay vì nhiều request lẻ.
// Chậm — tuần tự
teardown('cleanup users', async ({ request }) => {
for (const id of userIds) {
await request.delete(`/api/users/${id}`);
}
});
// Nhanh hơn — song song hoặc bulk
teardown('cleanup users', async ({ request }) => {
await request.delete('/api/test/users/bulk', {
data: { ids: userIds },
});
});
Quiz
Câu 1. Config có project main với teardown: 'cleanup'. Nếu 3 trong 20 test của main fail, teardown có chạy không?
Đáp án
Có. Teardown chạy sau khi project kết thúc — dù pass hay fail. Dù main có test fail, teardown vẫn được trigger để cleanup.
Câu 2. Project main có cả dependencies: ['setup'] và teardown: 'cleanup'. Nếu project setup fail, điều gì xảy ra với cleanup?
Đáp án
Setup fail → main bị skip → cleanup cũng bị skip. Playwright không trigger teardown khi main không được chạy do dependency fail.
Câu 3. Sự khác biệt cốt lõi giữa teardown project và afterAll hook về visibility trong report?
Đáp án
Teardown project có entry đầy đủ trong HTML reporter — thấy được từng teardown step, kết quả pass/fail, thời gian chạy. afterAll không có entry riêng trong reporter — nếu fail, lỗi gắn vào test output của describe block.
Câu 4. Tại sao nên dùng try/catch trong cleanup logic thay vì để throw?
Đáp án
Teardown là project thật — nếu throw uncaught error, CI exit code non-zero dù main test đã pass. Điều này có thể mask kết quả thật. Wrap try/catch và log warning giữ cleanup failure là non-fatal, không ảnh hưởng CI pass/fail.
Câu 5. Có 2 main project (project-A và project-B) cùng trỏ teardown: 'cleanup'. Playwright chạy cleanup bao nhiêu lần?
Đáp án
Một lần — sau khi cả hai project kết thúc. Playwright không chạy teardown project 2 lần riêng biệt cho từng main project trỏ đến nó.
