Global Setup and Teardown
When to use: One-time operations that must run before or after the entire test suite -- database seeding, environment health checks, creating shared auth state, starting external services. Runs once per
npx playwright testinvocation, not once per test or per worker.
Quick Reference
globalSetup → runs ONCE before all tests in all projects
↓
setup projects → runs before dependent projects (has browser context)
↓
test projects → your actual tests
↓
teardown projects → runs after dependent projects (has browser context)
↓
globalTeardown → runs ONCE after all tests in all projectsKey distinction:
globalSetup/globalTeardown: No browser, no Playwright fixtures. Pure Node.js.- Setup projects with
dependencies: Has full browser context, can usepage,request, etc.
Patterns
Pattern 1: Basic Global Setup and Teardown
Use when: One-time non-browser work like database seeding, environment validation, or external service preparation. Avoid when: You need a browser (use a setup project instead) or per-test isolation (use fixtures).
// playwright.config.ts
import { defineConfig } from "@playwright/test"
export default defineConfig({
globalSetup: "./tests/global-setup.ts",
globalTeardown: "./tests/global-teardown.ts",
testDir: "./tests",
})// tests/global-setup.ts
import type { FullConfig } from "@playwright/test"
async function globalSetup(config: FullConfig) {
console.log("Global setup: seeding database...")
// Seed the test database
const { execSync } = await import("child_process")
execSync("npx prisma db push --force-reset", { stdio: "inherit" })
execSync("npx prisma db seed", { stdio: "inherit" })
// Store run metadata for tests to use
process.env.TEST_RUN_ID = `run-${Date.now()}`
}
export default globalSetup// tests/global-teardown.ts
import type { FullConfig } from "@playwright/test"
async function globalTeardown(config: FullConfig) {
console.log("Global teardown: cleaning up...")
const { execSync } = await import("child_process")
execSync("npx prisma db push --force-reset", { stdio: "inherit" })
}
export default globalTeardownPattern 2: Environment Health Check in Global Setup
Use when: Verifying the test environment is healthy before running any tests. Fails fast if services are down. Avoid when: Tests use webServer which already does a health check.
// tests/global-setup.ts
import type { FullConfig } from "@playwright/test"
async function globalSetup(config: FullConfig) {
const baseURL = config.projects[0]?.use?.baseURL || "http://localhost:3000"
const maxRetries = 10
const retryDelay = 2000
console.log(`Checking if ${baseURL} is reachable...`)
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(`${baseURL}/api/health`)
if (response.ok) {
console.log(`Environment is healthy (attempt ${i + 1})`)
return
}
} catch {
// Connection refused or timeout -- retry
}
console.log(`Waiting for environment... (attempt ${i + 1}/${maxRetries})`)
await new Promise((resolve) => setTimeout(resolve, retryDelay))
}
throw new Error(`Environment at ${baseURL} is not reachable after ${maxRetries} attempts`)
}
export default globalSetupPattern 3: Authentication State in Global Setup (Without Browser)
Use when: Creating auth tokens or session cookies via API, without needing a browser. Avoid when: Login requires browser interaction (use a setup project instead).
// tests/global-setup.ts
import type { FullConfig } from "@playwright/test"
import * as fs from "fs"
import * as path from "path"
async function globalSetup(config: FullConfig) {
const baseURL = config.projects[0]?.use?.baseURL || "http://localhost:3000"
// Authenticate via API (no browser needed)
const response = await fetch(`${baseURL}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "admin@example.com",
password: process.env.TEST_PASSWORD,
}),
})
if (!response.ok) {
throw new Error(`Authentication failed: ${response.status} ${response.statusText}`)
}
const { token } = await response.json()
// Save the token as storageState for browser tests to pick up
const authDir = path.resolve(process.cwd(), "playwright/.auth")
fs.mkdirSync(authDir, { recursive: true })
const storageState = {
cookies: [],
origins: [
{
origin: baseURL,
localStorage: [{ name: "auth_token", value: token }],
},
],
}
fs.writeFileSync(path.join(authDir, "user.json"), JSON.stringify(storageState, null, 2))
}
export default globalSetup// playwright.config.ts
import { defineConfig, devices } from "@playwright/test"
export default defineConfig({
globalSetup: "./tests/global-setup.ts",
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
storageState: "playwright/.auth/user.json",
},
},
],
})Pattern 4: Passing Data from Global Setup to Tests
Use when: Global setup generates values (IDs, tokens, URLs) that tests need. Avoid when: Each test should create its own data (the usual case).
Method 1: Environment variables (simplest):
// tests/global-setup.ts
import type { FullConfig } from "@playwright/test"
async function globalSetup(config: FullConfig) {
process.env.TEST_RUN_ID = `run-${Date.now()}`
process.env.SEED_USER_ID = "user-12345"
}
export default globalSetup// tests/dashboard.spec.ts
import { test, expect } from "@playwright/test"
test("dashboard shows seeded data", async ({ page }) => {
const userId = process.env.SEED_USER_ID
await page.goto(`/users/${userId}/dashboard`)
await expect(page.getByRole("heading")).toBeVisible()
})Method 2: Shared file (for complex data):
// tests/global-setup.ts
import type { FullConfig } from "@playwright/test"
import * as fs from "fs"
import * as path from "path"
const SETUP_DATA_PATH = path.resolve(process.cwd(), "test-data/setup-data.json")
async function globalSetup(config: FullConfig) {
const baseURL = config.projects[0]?.use?.baseURL || "http://localhost:3000"
// Create test data via API
const res = await fetch(`${baseURL}/api/test/seed`, { method: "POST" })
const seedData = await res.json()
// Write to shared file
const dir = path.dirname(SETUP_DATA_PATH)
fs.mkdirSync(dir, { recursive: true })
fs.writeFileSync(SETUP_DATA_PATH, JSON.stringify(seedData, null, 2))
}
export default globalSetup// tests/helpers/setup-data.ts
import * as fs from "fs"
import * as path from "path"
const SETUP_DATA_PATH = path.resolve(process.cwd(), "test-data/setup-data.json")
export function getSetupData(): { userId: string; orgId: string; apiKey: string } {
const raw = fs.readFileSync(SETUP_DATA_PATH, "utf8")
return JSON.parse(raw)
}// tests/org-settings.spec.ts
import { test, expect } from "@playwright/test"
import { getSetupData } from "./helpers/setup-data"
test("org settings page loads", async ({ page }) => {
const { orgId } = getSetupData()
await page.goto(`/orgs/${orgId}/settings`)
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible()
})Pattern 5: Global Setup with storageState (Browser-Based Auth)
Use when: Authentication requires browser interaction (form login, OAuth redirect, MFA). Avoid when: Auth can be done via API call (use Pattern 3 instead).
Important: globalSetup has no browser. For browser-based auth, use a setup project instead.
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test"
export default defineConfig({
projects: [
// Setup project: runs first, has a browser, saves auth state
{
name: "setup",
testMatch: /global\.setup\.ts/,
},
// Test projects: depend on setup, reuse saved auth state
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
storageState: "playwright/.auth/user.json",
},
dependencies: ["setup"],
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
storageState: "playwright/.auth/user.json",
},
dependencies: ["setup"],
},
],
})// tests/global.setup.ts
import { test as setup, expect } from "@playwright/test"
const authFile = "playwright/.auth/user.json"
setup("authenticate", async ({ page }) => {
await page.goto("/login")
await page.getByLabel("Email").fill("user@example.com")
await page.getByLabel("Password").fill(process.env.TEST_PASSWORD!)
await page.getByRole("button", { name: "Sign in" }).click()
// Wait for navigation to confirm login succeeded
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible()
// Save signed-in state (cookies + localStorage)
await page.context().storageState({ path: authFile })
})Pattern 6: Global Setup for External Services
Use when: Starting or configuring external services (mock servers, test containers, feature flags) before any tests run. Avoid when: Per-test or per-worker isolation is needed (use fixtures).
// tests/global-setup.ts
import type { FullConfig } from "@playwright/test"
async function globalSetup(config: FullConfig) {
// Start a mock API server
const { createServer } = await import("../mocks/server")
const server = await createServer()
const port = await server.listen(0)
process.env.MOCK_API_URL = `http://localhost:${port}`
// Configure feature flags for test environment
const baseURL = config.projects[0]?.use?.baseURL || "http://localhost:3000"
await fetch(`${baseURL}/api/admin/feature-flags`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.ADMIN_API_KEY}`,
},
body: JSON.stringify({
newCheckout: true,
darkMode: false,
betaFeatures: true,
}),
})
// Return a cleanup function (Playwright calls globalTeardown separately)
// For cleanup, use globalTeardown
}
export default globalSetup// tests/global-teardown.ts
import type { FullConfig } from "@playwright/test"
async function globalTeardown(config: FullConfig) {
// Reset feature flags
const baseURL = config.projects[0]?.use?.baseURL || "http://localhost:3000"
await fetch(`${baseURL}/api/admin/feature-flags/reset`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ADMIN_API_KEY}` },
})
}
export default globalTeardownDecision Guide
| Need | Use | Why |
|---|---|---|
| One-time DB seed | globalSetup | No browser needed; runs once before everything |
| Browser-based login (shared state) | Setup project with dependencies | Needs page and context (not available in globalSetup) |
| API-based auth token | globalSetup | Simple HTTP call, no browser needed |
| Per-test unique data | Custom fixture via test.extend() | Each test gets isolated data |
| Per-worker shared resource | Worker-scoped fixture (\{ scope: 'worker' \}) | Shared within worker, isolated between workers |
| Health check before tests | globalSetup | Fail fast if environment is down |
| Start mock server | globalSetup + globalTeardown | One-time server lifecycle |
| Clean up after all tests | globalTeardown | Runs once at the end regardless of pass/fail |
Do I need globalSetup?
│
├── Does the work need a browser (page, context)?
│ ├── YES → Use a setup project, not globalSetup
│ └── NO → globalSetup is appropriate
│
├── Does every test need unique/isolated data?
│ ├── YES → Use a fixture with test.extend()
│ └── NO → globalSetup for shared, read-only data
│
├── Is it per-worker (expensive resource, connection pool)?
│ ├── YES → Worker-scoped fixture
│ └── NO → globalSetup for truly global, one-time work
│
└── Is it cleanup?
├── After all tests → globalTeardown
├── After each test → Fixture teardown (after use())
└── After each worker → Worker-scoped fixture teardownAnti-Patterns
| Anti-Pattern | Problem | Do This Instead |
|---|---|---|
Browser login in globalSetup | No browser context available; complex workarounds | Use a setup project with dependencies |
Creating per-test data in globalSetup | All tests share the same data; not isolated | Use per-test fixtures for unique data |
globalSetup without globalTeardown | Database or services left in dirty state | Always pair setup with teardown |
| Storing setup results in module-level variables | Workers are separate processes; variables don't share | Use environment variables or files |
Complex logic in globalSetup | Hard to debug; runs outside normal test lifecycle | Keep it minimal: seed, verify, set env vars |
globalSetup that takes > 60 seconds | Slows every test run, even for a single test | Move heavy work to a separate script or CI step |
Relying on globalTeardown for critical cleanup | If the process crashes, globalTeardown might not run | Design tests to be idempotent; use beforeAll in setup project |
Troubleshooting
Global setup runs but environment variables are not available in tests
Cause: Each worker is a separate process. process.env mutations in globalSetup propagate to workers, but only if set before workers spawn.
Fix: Set environment variables at the top level of globalSetup, before any async work that might delay:
// tests/global-setup.ts
async function globalSetup() {
// This works -- set before returning
process.env.TEST_RUN_ID = `run-${Date.now()}`
}
export default globalSetupIf environment variables are still missing, write data to a file instead (Pattern 4, Method 2).
Global setup fails with "Cannot find module"
Cause: The path in globalSetup is relative to the config file, but the module resolution is wrong.
Fix: Use a path relative to the project root:
// playwright.config.ts
import { defineConfig } from "@playwright/test"
export default defineConfig({
globalSetup: "./tests/global-setup.ts", // relative to config location
globalTeardown: "./tests/global-teardown.ts",
})Global teardown doesn't run after test failure
Cause: If the process is killed (SIGKILL, OOM) rather than exiting normally, teardown is skipped.
Fix: Design your setup to be idempotent. Global setup should handle a dirty state from a previous incomplete run:
// tests/global-setup.ts
async function globalSetup() {
// Always reset first, then seed -- handles dirty state
const { execSync } = await import("child_process")
execSync("npx prisma db push --force-reset", { stdio: "inherit" })
execSync("npx prisma db seed", { stdio: "inherit" })
}
export default globalSetupSetup project runs every time, even when only running one test file
Cause: The dependencies configuration requires the setup project to run before any dependent project.
Fix: This is expected behavior. To skip setup during focused debugging:
# Skip setup by running without dependencies
npx playwright test --project=chromium --no-deps tests/specific-test.spec.tsstorageState file doesn't exist when tests start
Cause: The setup project or globalSetup that creates the file failed silently, or the path is wrong.
Fix: Add explicit error handling and verify the file exists:
// tests/global.setup.ts
import { test as setup, expect } from "@playwright/test"
import * as fs from "fs"
const authFile = "playwright/.auth/user.json"
setup("authenticate", async ({ page }) => {
await page.goto("/login")
await page.getByLabel("Email").fill("user@example.com")
await page.getByLabel("Password").fill(process.env.TEST_PASSWORD!)
await page.getByRole("button", { name: "Sign in" }).click()
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible()
await page.context().storageState({ path: authFile })
// Verify the file was created
if (!fs.existsSync(authFile)) {
throw new Error(`Auth state file was not created at ${authFile}`)
}
})Related
- core/fixtures-and-hooks.md -- per-test and per-worker fixtures (preferred over globalSetup for most cases)
- core/configuration.md --
globalSetup,globalTeardown,webServerconfig - ci/projects-and-dependencies.md -- setup projects with
dependencies - core/authentication.md -- authentication patterns using setup projects
- core/test-data-management.md -- seeding and managing test data