Back to Sanity

Global Setup & Teardown

.agents/skills/playwright-best-practices/core/global-setup.md

5.20.012.5 KB
Original Source

Global Setup & Teardown

Table of Contents

  1. Global Setup
  2. Global Teardown
  3. Database Patterns
  4. Environment Provisioning
  5. Setup Projects vs Global Setup
  6. Parallel Execution Caveats

Global Setup

Basic Global Setup

typescript
// global-setup.ts
import {FullConfig} from '@playwright/test'

async function globalSetup(config: FullConfig) {
  console.log('Running global setup...')
  // Perform one-time setup: start services, run migrations, etc.
}

export default globalSetup

Configure Global Setup

typescript
// playwright.config.ts
import {defineConfig} from '@playwright/test'

export default defineConfig({
  globalSetup: require.resolve('./global-setup'),
  globalTeardown: require.resolve('./global-teardown'),
})

Authentication in Global Setup: For authentication patterns using storage state in global setup, see fixtures-hooks.md. Setup projects are generally preferred for authentication as they provide access to Playwright fixtures.

Global Setup with Return Value

typescript
// global-setup.ts
async function globalSetup(config: FullConfig): Promise<() => Promise<void>> {
  const server = await startTestServer()

  // Return cleanup function (alternative to globalTeardown)
  return async () => {
    await server.stop()
  }
}

export default globalSetup

Access Config in Global Setup

typescript
// global-setup.ts
import {FullConfig} from '@playwright/test'

async function globalSetup(config: FullConfig) {
  const {baseURL} = config.projects[0].use
  console.log(`Setting up for ${baseURL}`)

  // Access custom config
  const workers = config.workers
  const timeout = config.timeout

  // Access environment
  const isCI = !!process.env.CI
}

export default globalSetup

Global Teardown

Basic Global Teardown

typescript
// global-teardown.ts
import {FullConfig} from '@playwright/test'
import fs from 'fs'

async function globalTeardown(config: FullConfig) {
  console.log('Running global teardown...')

  // Clean up auth files
  if (fs.existsSync('.auth')) {
    fs.rmSync('.auth', {recursive: true})
  }

  // Clean up test data
  await cleanupTestDatabase()

  // Stop services
  await stopTestServices()
}

export default globalTeardown

Conditional Teardown

typescript
// global-teardown.ts
async function globalTeardown(config: FullConfig) {
  // Skip cleanup in CI (containers are discarded anyway)
  if (process.env.CI) {
    console.log('Skipping teardown in CI')
    return
  }

  // Local cleanup
  await cleanupLocalTestData()
}

export default globalTeardown

Database Patterns

This section covers one-time database setup (migrations, snapshots, per-worker databases). For related topics:

Database Migration in Setup

typescript
// global-setup.ts
import {execSync} from 'child_process'

async function globalSetup() {
  console.log('Running database migrations...')

  // Run migrations
  execSync('npx prisma migrate deploy', {stdio: 'inherit'})

  // Seed test data
  execSync('npx prisma db seed', {stdio: 'inherit'})
}

export default globalSetup

Database Snapshot Pattern

typescript
// global-setup.ts
import {execSync} from 'child_process'
import fs from 'fs'

const SNAPSHOT_PATH = './test-db-snapshot.sql'

async function globalSetup() {
  // Check if snapshot exists
  if (fs.existsSync(SNAPSHOT_PATH)) {
    console.log('Restoring database from snapshot...')
    execSync(`psql $DATABASE_URL < ${SNAPSHOT_PATH}`, {stdio: 'inherit'})
    return
  }

  // First run: migrate and create snapshot
  console.log('Creating database snapshot...')
  execSync('npx prisma migrate deploy', {stdio: 'inherit'})
  execSync('npx prisma db seed', {stdio: 'inherit'})
  execSync(`pg_dump $DATABASE_URL > ${SNAPSHOT_PATH}`, {stdio: 'inherit'})
}

export default globalSetup

Test Database per Worker

typescript
// global-setup.ts
async function globalSetup(config: FullConfig) {
  const workerCount = config.workers || 1

  // Create a database for each worker
  for (let i = 0; i < workerCount; i++) {
    const dbName = `test_db_worker_${i}`
    await createDatabase(dbName)
    await runMigrations(dbName)
    await seedDatabase(dbName)
  }
}

// global-teardown.ts
async function globalTeardown(config: FullConfig) {
  const workerCount = config.workers || 1

  for (let i = 0; i < workerCount; i++) {
    await dropDatabase(`test_db_worker_${i}`)
  }
}

Environment Provisioning

Start Services in Setup

typescript
// global-setup.ts
import {execSync, spawn} from 'child_process'

let serverProcess: any

async function globalSetup() {
  // Start backend server
  serverProcess = spawn('npm', ['run', 'start:test'], {
    stdio: 'pipe',
    detached: true,
  })

  // Wait for server to be ready
  await waitForServer('http://localhost:3000/health', 30000)

  // Store PID for teardown
  process.env.SERVER_PID = serverProcess.pid.toString()
}

async function waitForServer(url: string, timeout: number) {
  const start = Date.now()

  while (Date.now() - start < timeout) {
    try {
      const response = await fetch(url)
      if (response.ok) return
    } catch {
      // Server not ready yet
    }
    await new Promise((r) => setTimeout(r, 1000))
  }

  throw new Error(`Server did not start within ${timeout}ms`)
}

export default globalSetup

Docker Compose Setup

typescript
// global-setup.ts
import {execSync} from 'child_process'

async function globalSetup() {
  console.log('Starting Docker services...')

  execSync('docker-compose -f docker-compose.test.yml up -d', {
    stdio: 'inherit',
  })

  // Wait for services to be healthy
  execSync('docker-compose -f docker-compose.test.yml exec -T db pg_isready', {
    stdio: 'inherit',
  })
}

export default globalSetup
typescript
// global-teardown.ts
import {execSync} from 'child_process'

async function globalTeardown() {
  console.log('Stopping Docker services...')

  execSync('docker-compose -f docker-compose.test.yml down -v', {
    stdio: 'inherit',
  })
}

export default globalTeardown

Environment Variables Setup

typescript
// global-setup.ts
import dotenv from 'dotenv'
import path from 'path'

async function globalSetup() {
  // Load test-specific environment
  const envFile = process.env.CI ? '.env.ci' : '.env.test'
  dotenv.config({path: path.resolve(process.cwd(), envFile)})

  // Validate required variables
  const required = ['DATABASE_URL', 'API_KEY', 'TEST_EMAIL']
  for (const key of required) {
    if (!process.env[key]) {
      throw new Error(`Missing required environment variable: ${key}`)
    }
  }
}

export default globalSetup

Setup Projects vs Global Setup

When to Use Each

Use Global SetupUse Setup Projects
One-time setup (migrations, services)Per-project setup (auth states)
No access to Playwright fixturesNeed page, request fixtures
Runs once before all projectsCan run per-project or have dependencies
Shared across all workersCan be parallelized

Setup Project Pattern

typescript
// playwright.config.ts
export default defineConfig({
  projects: [
    // Setup project
    {
      name: 'setup',
      testMatch: /.*\.setup\.ts/,
    },
    // Test projects depend on setup
    {
      name: 'chromium',
      use: {...devices['Desktop Chrome']},
      dependencies: ['setup'],
    },
    {
      name: 'firefox',
      use: {...devices['Desktop Firefox']},
      dependencies: ['setup'],
    },
  ],
})

For complete authentication setup patterns, see fixtures-hooks.md.

Combining Both

typescript
// playwright.config.ts
export default defineConfig({
  // Global: Start services, run migrations
  globalSetup: require.resolve('./global-setup'),
  globalTeardown: require.resolve('./global-teardown'),

  projects: [
    // Setup project: Create auth states
    {name: 'setup', testMatch: /.*\.setup\.ts/},
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: '.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
})

Parallel Execution Caveats

Understanding Global Setup Execution

┌─────────────────────────────────────────────────────────────┐
│  globalSetup runs ONCE                                      │
│  ↓                                                          │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐        │
│  │ Worker 1│  │ Worker 2│  │ Worker 3│  │ Worker 4│        │
│  │ tests   │  │ tests   │  │ tests   │  │ tests   │        │
│  └─────────┘  └─────────┘  └─────────┘  └─────────┘        │
│  ↓                                                          │
│  globalTeardown runs ONCE                                   │
└─────────────────────────────────────────────────────────────┘

Key implications:

  • Global setup has no access to Playwright fixtures (page, request, context)
  • State created in global setup is shared across all workers
  • If tests modify shared state, they may conflict with parallel workers
  • Global setup cannot react to individual test needs

When to Prefer Worker-Scoped Fixtures

Use worker-scoped fixtures instead of globalSetup when:

ScenarioWhy Fixtures Are Better
Each worker needs isolated resourcesFixtures can create per-worker databases, servers
Setup needs Playwright APIsFixtures have access to page, request, browser
Setup depends on test configurationFixtures receive test context and options
Resources need cleanup per workerWorker fixtures auto-cleanup when worker exits

Common Parallel Pitfall

typescript
// ❌ BAD: Global setup creates ONE user, all workers fight over it
async function globalSetup() {
  await createUser({email: '[email protected]'}) // Shared!
}

// ✅ GOOD: Each worker gets its own user via worker-scoped fixture
// Uses workerInfo.workerIndex to create unique data per worker

For worker-scoped fixture patterns (per-worker databases, unique test data, workerIndex isolation), see fixtures-hooks.md.

Anti-Patterns to Avoid

Anti-PatternProblemSolution
Heavy setup in globalSetupSlow test startupUse setup projects for parallelizable work
Not cleaning up in teardownLeaks resources, flaky CIAlways clean up or use containers
Hardcoded URLs in setupBreaks in different environmentsUse config.projects[0].use.baseURL
No timeout on service waitHangs forever if service failsAdd timeout with clear error
Shared mutable stateRace conditions in parallelUse worker-scoped fixtures for isolation
Global setup for per-test dataTests conflictUse test-scoped fixtures