Global Setup and Teardown in Playwright: Seeding and Health Checks
If every test file in your suite re-seeds the database, re-logs in, and prays the backend is awake, you are paying that cost hundreds of times per run. Playwright global setup teardown lets you do expensive, environment-wide work exactly once before any test starts, and tear it down cleanly after the last test finishes. In this guide you will learn how to seed a database, run a pre-flight health check that fails fast, share an authenticated session across projects, and clean up reliably even when tests crash.
🎠Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.
Contents
Why You Need Global Setup and Teardown
Playwright already gives you per-file and per-test hooks like beforeAll, beforeEach, and fixtures. Those are perfect for work scoped to a single spec. But some work is genuinely process-wide: confirming the API is reachable, applying database migrations, seeding reference data, or generating an auth token that every test reuses. Repeating that in every file is slow and flaky. Global setup runs in a separate Node.js context before the worker processes spawn, so it is the natural home for one-time, cross-cutting preparation.
There are two ways to wire this up. The classic approach uses the globalSetup and globalTeardown keys in playwright.config.ts. The modern approach uses setup projects with dependencies, which gives you trace, retries, and full fixtures inside your setup code. We will cover both, and explain when each one wins.
The Classic Approach: globalSetup and globalTeardown
The simplest form points your config at two files. Each is a Node module that default-exports an async function. The setup function receives the resolved FullConfig, and anything it returns is ignored, so it communicates with tests through side effects: environment variables, files on disk, or a seeded backend.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
globalSetup: require.resolve('./global-setup'),
globalTeardown: require.resolve('./global-teardown'),
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
},
});
Now the setup file itself. Below it performs a health check and seeds data, then stashes a value other tests can read. Note the signature: a function that takes FullConfig and returns a Promise.
// global-setup.ts
import type { FullConfig } from '@playwright/test';
import { request } from '@playwright/test';
async function globalSetup(config: FullConfig) {
const baseURL =
config.projects[0].use.baseURL ?? 'http://localhost:3000';
// 1) Health check: fail the whole run fast if the app is down.
const api = await request.newContext({ baseURL });
const health = await api.get('/api/health');
if (!health.ok()) {
throw new Error(
`Health check failed: ${health.status()} from ${baseURL}/api/health`,
);
}
// 2) Seed reference data once for the entire suite.
const seed = await api.post('/api/test/seed', {
data: { users: 5, products: 20 },
});
if (!seed.ok()) {
throw new Error(`Seeding failed: ${seed.status()}`);
}
// 3) Share state with tests via an env var (or write a file).
const body = await seed.json();
process.env.SEED_RUN_ID = body.runId;
await api.dispose();
}
export default globalSetup;
The matching teardown reads the same value and cleans up. Playwright runs globalTeardown after all tests complete, even if some failed, which makes it the right place to drop seeded rows or revoke tokens.
// global-teardown.ts
import { request } from '@playwright/test';
async function globalTeardown() {
const runId = process.env.SEED_RUN_ID;
if (!runId) return;
const api = await request.newContext({
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
});
await api.delete(`/api/test/seed/${runId}`);
await api.dispose();
}
export default globalTeardown;
A subtle but important detail: environment variables set in global setup are visible to teardown because they share the same Node process, but they are not automatically injected into the test worker processes in every runner version. For data you must read inside tests, prefer writing a JSON file to disk (for example .auth/state.json) and re
