PRACTICAL GUIDE / playwright fixtures explained

Playwright Fixtures Explained: Test Setup That Scales

Playwright fixtures explained with test scope, worker scope, custom setup, cleanup, auth state, examples, reporting, and mistakes to avoid in CI.

By The Testing AcademyUpdated July 10, 20268 min read
All field guides
In this guide8 sections
  1. Read a Fixture as a Lifecycle
  2. Choose Scope by Isolation Risk
  3. Extend the Base Test With Domain Capabilities
  4. Model Authentication at the Right Boundary
  5. Use Options for Configuration, Fixtures for Resources
  6. Apply Automatic Fixtures Sparingly
  7. Avoid Fixture Designs That Create Flake
  8. Migrate Setup With an Ownership Checklist

What you will learn

  • Read a Fixture as a Lifecycle
  • Choose Scope by Isolation Risk
  • Extend the Base Test With Domain Capabilities
  • Model Authentication at the Right Boundary

Test setup becomes dangerous when nobody can tell who owns it. A beforeEach signs in, a helper creates an order, a project dependency seeds an account, and a retry inherits whatever cleanup missed. Playwright fixtures provide a dependency model for that work, but moving every setup line into a fixture can make the ownership problem worse.

Use a fixture when a test needs a named capability with a clear lifecycle. The test asks for that capability in its parameters, Playwright prepares its dependencies, and teardown runs after the consumer finishes. That explicit request is the feature that makes fixtures scale.

Read a Fixture as a Lifecycle

Built-in fixtures such as page, context, browser, request, and testInfo already follow this model. A custom fixture has three phases:

  1. Prepare the resource.
  2. Pass it to the test by calling use(resource).
  3. Clean up after use returns.
TypeScript
import { test as base, expect } from '@playwright/test'

type Fixtures = {
  account: { id: string; email: string; password: string }
}

export const test = base.extend<Fixtures>({
  account: async ({ request }, use) => {
    const response = await request.post('/api/test/accounts', {
      data: { role: 'buyer' },
    })
    expect(response.ok()).toBeTruthy()
    const account = await response.json()

    await use(account)

    await request.delete(`/api/test/accounts/${account.id}`)
  },
})

export { expect }

If the test throws, the code after use still gets its opportunity to run. Cleanup should tolerate partial setup and resources that the test legitimately deleted. A teardown failure must not erase the original test evidence, so log enough context to distinguish them.

Choose Scope by Isolation Risk

The default fixture scope is test: Playwright creates one instance for each test that requests it. A worker-scoped fixture is created once for a worker process and reused by tests scheduled in that worker.

ResourceLikely scopeDecision test
New customer accountTestCases mutate identity or data
Page object wrapping pageTestIt depends on a test-scoped page
Unique orderTestEach case needs independent state
Read-only service clientWorkerSafe to share and costly to initialize
Database schema for one workerWorkerNamespaced so workers cannot collide
Browser page or contextTestCookies, storage, and navigation mutate

Worker scope is an optimization with a cost. Shared mutable state can create failures that depend on which tests happen to share a worker. Retries may run in a new worker, so a worker fixture can be recreated even if it was intended to exist “once.”

Use workerInfo.workerIndex or parallelIndex to namespace worker-owned accounts, schemas, or ports. Do not assume one worker equals one spec file or one permanent CI machine.

Extend the Base Test With Domain Capabilities

A fixture file becomes the suite's import boundary. Specs should import the extended test and expect, not mix them accidentally with the base package.

TypeScript
import { test as base, expect, type Page } from '@playwright/test'

class OrdersPage {
  constructor(private readonly page: Page) {}

  async open() {
    await this.page.goto('/orders')
  }

  rowFor(orderId: string) {
    return this.page.getByRole('row', { name: new RegExp(orderId) })
  }
}

type AppFixtures = {
  ordersPage: OrdersPage
}

export const test = base.extend<AppFixtures>({
  ordersPage: async ({ page }, use) => {
    await use(new OrdersPage(page))
  },
})

export { expect }

The fixture constructs a capability. It does not automatically navigate, create orders, or assert that the page loaded. A test that requests ordersPage should still show the scenario:

TypeScript
test('new order appears in history', async ({ ordersPage }) => {
  await ordersPage.open()
  await expect(ordersPage.rowFor('ORD-1042')).toBeVisible()
})

When fixtures depend on one another, Playwright builds only the dependency graph needed by the requested test. That is more focused than a large beforeEach that initializes every object for every case.

Model Authentication at the Right Boundary

Authentication is a common fixture, but there are two different needs. A login test should exercise the login UI. Most other tests need an already authenticated context.

For a single shared role, generate storageState in a setup project and configure dependent projects to use it. For unique or dynamic users, create a test-scoped account and authenticate through an API or controlled setup path, then create a context with that state.

TypeScript
type AuthFixtures = {
  authenticatedPage: Page
}

export const test = base.extend<AuthFixtures>({
  authenticatedPage: async ({ browser, request, account }, use) => {
    const login = await request.post('/api/auth/login', {
      data: { email: account.email, password: account.password },
    })
    expect(login.ok()).toBeTruthy()

    const state = await request.storageState()
    const context = await browser.newContext({ storageState: state })
    const page = await context.newPage()

    await use(page)

    await context.close()
  },
})

Confirm that the API request context and browser context share the authentication mechanism your application uses. Cookies, origins, and tokens must line up. Do not copy a token into local storage without understanding when the app reads and refreshes it.

Shared accounts are unsuitable when tests modify profile, permissions, cart, or subscription state. A cached admin identity may be fine for read-only reporting checks, while buyer scenarios need unique accounts.

Use Options for Configuration, Fixtures for Resources

Fixture options let projects or individual tests provide typed configuration without reading environment variables throughout the suite.

TypeScript
type Options = {
  defaultRole: 'buyer' | 'seller'
}

export const test = base.extend<Options>({
  defaultRole: ['buyer', { option: true }],
})

The project can set defaultRole through use, and a describe block can override it with test.use({ defaultRole: 'seller' }). A resource fixture can depend on that option when it creates an account.

This separation keeps concerns clear. An option describes what the test needs. A fixture performs work to provide it. Avoid reading process.env.ROLE inside five different fixtures, because the effective configuration becomes difficult to report and override.

Options should remain serializable, predictable inputs. Secrets still belong in protected environment configuration and should never be attached to test output.

Apply Automatic Fixtures Sparingly

An automatic fixture runs even when the test does not list it. This can be useful for universal diagnostics, such as attaching application logs after a failure.

TypeScript
type AutoFixtures = {
  captureAppLogs: void
}

export const test = base.extend<AutoFixtures>({
  captureAppLogs: [async ({ page }, use, testInfo) => {
    const messages: string[] = []
    page.on('console', message => messages.push(message.text()))

    await use()

    if (testInfo.status !== testInfo.expectedStatus) {
      await testInfo.attach('browser-console', {
        body: Buffer.from(messages.join('\n')),
        contentType: 'text/plain',
      })
    }
  }, { auto: true }],
})

Automatic fixtures should be cheap and genuinely universal. Auto-login, auto-navigation, and auto-data creation make test parameters lie about dependencies. They also impose work on tests that do not need it.

Review fixture names in reports. If setup dominates traces, consider whether the fixture does too much or whether its scope is wrong. Setup should improve diagnosis, not become an opaque prelude to every test.

Avoid Fixture Designs That Create Flake

The most costly mistakes are lifecycle mistakes:

  • A worker fixture returns a mutable user that tests edit concurrently.
  • Cleanup uses the page after the page or context fixture has already closed.
  • A fixture catches setup errors and calls use with an incomplete object.
  • An automatic fixture changes application state for every test.
  • A fixture contains the primary product assertion, so the report says setup failed.
  • One giant fixture exposes unrelated pages, clients, users, and test data.
  • Tests import test from different fixture modules and silently lose extensions.
  • Teardown assumes setup completed fully and masks the initial exception.

Dependency order follows fixture use, so design resources to clean up while their dependencies are still available. If an order fixture needs an API client to delete its record, express that dependency in its parameter list rather than importing a global client.

Retries are a useful audit. If a retry passes only because it receives a fresh worker or account, the original case probably leaked or depended on state.

Migrate Setup With an Ownership Checklist

Do not convert every hook in one pass. Pick a repeated setup capability and write down its inputs, output, scope, dependencies, and cleanup. Implement it as test-scoped first. Run cases in parallel and with retries before considering worker scope.

For every new fixture, verify:

  • A test requests it by a name that explains the capability.
  • Its scope matches how the resource can be mutated.
  • Setup fails immediately when the resource cannot be prepared.
  • Teardown is safe after partial setup and test-side deletion.
  • Parallel workers receive unique data or isolated namespaces.
  • The main product assertion remains in the spec.
  • Imports consistently use the extended test module.

The next practical step is to replace one repeated authentication or data-seeding hook with a test-scoped fixture, then inspect a passing trace, a setup failure, and a teardown failure. If all three tell a clear story, the fixture is ready to become part of the suite's shared contract.

// FIELD DISPATCH

Get the QA Field Notes

Weekly QA battles, AI testing guides, and interview drills. Free on Substack.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 10, 2026 / Reviewed July 10, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Playwright documentation

    Microsoft

    Canonical API, locator, fixture, browser, and test-runner behavior.

  2. 02
    Playwright best practices

    Microsoft

    Official guidance for resilient tests, isolation, and user-facing locators.

  3. 03
    WebDriver standard

    W3C

    The browser automation protocol specification used by major automation stacks.

FAQ / QUICK ANSWERS

Questions testers ask

What are fixtures in Playwright?

Fixtures are reusable setup and teardown building blocks that Playwright injects into tests. Built in fixtures include page, browser, context, request, and testInfo. Custom fixtures let you prepare logged in users, seeded data, page objects, and service clients consistently.

What is the difference between test and worker fixtures?

A test scoped fixture is created separately for each test. A worker scoped fixture is created once for a worker process and shared by tests running in that worker. Use worker scope for expensive setup that is safe to share.

Should page objects be Playwright fixtures?

Page objects can be fixtures when many tests need the same pages and setup pattern. Keep them thin and readable. Do not hide important assertions or business decisions inside fixtures because that makes tests harder to review.

How do fixtures help with flaky tests?

Fixtures reduce flakiness by centralizing setup, cleanup, authentication, data seeding, and environment checks. When each test starts from a known state, failures are more likely to reflect product behavior instead of polluted data.

Can Playwright fixtures replace beforeEach?

Fixtures can replace many beforeEach blocks, especially when setup is reusable and dependency based. beforeEach is still fine for simple local setup, but fixtures scale better when several tests need the same prepared object.