Q12 of 42 · Playwright

What is a fixture in Playwright and how does it differ from beforeEach?

PlaywrightMidplaywrightfixturestest-runnermid

Short answer

Short answer: A fixture is a named, scoped setup/teardown unit injected into tests by the runner. `beforeEach` runs setup imperatively for every test in the file. Fixtures are reusable, composable, can be worker-scoped, and override per-file via `test.use()`. Use fixtures for cross-file reuse; `beforeEach` for one-off setup.

Detail

Playwright's fixture model goes beyond Mocha-style hooks. The key concepts:

Built-in fixtures: page, browser, context, request are fixtures the runner injects into every test. They're scoped: page per-test, browser per-worker.

Custom fixtures: extend test to define your own. They live in a single file and any test that imports the extended test gets them.

import { test as base } from '@playwright/test';

type Fixtures = { authedPage: Page };

export const test = base.extend<Fixtures>({
  authedPage: async ({ page }, use) => {
    await page.goto('/login');
    await page.fill('[data-test=email]', 'alice@x.com');
    await page.fill('[data-test=password]', 'pwd');
    await page.click('[data-test=submit]');
    await use(page);  // pass to the test
    // teardown after `use` returns
  },
});

// In a spec
test('uses an already-authed page', async ({ authedPage }) => {
  await authedPage.goto('/dashboard');
});

Why fixtures beat beforeEach for reuse:

  • A fixture is named and self-contained; cross-file reuse is just an import.
  • Fixtures compose. authedPage can depend on apiClient, which depends on baseURL. The runner resolves the dependency graph.
  • Per-file overrides via test.use({ ... }) change the fixture's behaviour for that file only.
  • Fixtures can be worker-scoped (run once per worker) for expensive setup like database seeding.

beforeEach is still useful for spec-local setup that doesn't generalise — visiting a specific page, clearing local state. Don't fixturise everything.

Worker-scoped fixtures for one-time-per-worker setup:

export const test = base.extend<{}, { seedDb: void }>({
  seedDb: [async ({}, use) => {
    await db.seed();
    await use();
    await db.cleanup();
  }, { scope: 'worker' }],
});

The { scope: 'worker' } annotation makes it run once per worker process, not per test.

// WHAT INTERVIEWERS LOOK FOR

Knowing fixtures vs hooks, the per-test vs per-worker scope, that fixtures compose, and `test.use()` for per-file overrides.

// COMMON PITFALL

Re-implementing setup in every spec's `beforeEach` instead of extracting a fixture — duplicates code and skips the dependency-injection benefits.