Playwright Fixtures: A Complete Guide with Working Examples
Playwright fixtures explained — what they are, how they differ from before/after hooks, custom fixture patterns, the page object model, and how to inject test data cleanly.
· 10 min read
Fixtures are the most powerful concept in Playwright — and the most under-used. Understand them and your test suite becomes 50% smaller, dramatically more readable, and much easier to maintain. This guide covers what they are, how to write your own, and the patterns that scale.
What is a fixture in Playwright?
A fixture is a piece of test setup that Playwright provides to your test on demand. The built-in `page`, `browser`, `context`, and `request` parameters you destructure in every `test(...)` call are all fixtures.
Playwright sets them up before your test runs, tears them down after, and shares the heavy ones (like `browser`) across tests for performance. You can write your own.
Fixtures vs beforeEach / afterEach hooks
Hooks are imperative and global. Fixtures are declarative and per-test. If a test does not destructure a fixture, the setup does not run for that test — automatic, no `if` statements.
Hooks also share state through closure variables, which gets messy. Fixtures give each test its own value and clean up automatically.
Writing a custom fixture
Extend Playwright's base `test`: `export const test = base.extend<{ loggedInPage: Page }>({ loggedInPage: async ({ page }, use) => { await page.goto('/login'); await page.fill('#email', '...'); await page.fill('#password', '...'); await page.click('button[type=submit]'); await use(page); } });`
Now any test that destructures `{ loggedInPage }` gets a logged-in browser page automatically. Tests that do not, do not pay the cost.
How to use fixtures for test data
The cleanest pattern: write a `testData` fixture that fetches the credentials, IDs, or payloads your test needs from a hosted catalog at runtime.
With TestAssets: `testData: async ({}, use) => { const res = await fetch('https://api.testassets.io/assets?list=staging-logins', { headers: { apikey: process.env.TESTASSETS_KEY } }); await use(await res.json()); }`. Now no test in your suite has hardcoded credentials, and rotating staging passwords is a single change in one place.
Page Object Model with fixtures
POM in Playwright is just a fixture pattern. Define a `LoginPage` class with methods like `goto()` and `login(email, password)`. Expose it as a fixture: `loginPage: async ({ page }, use) => { await use(new LoginPage(page)); }`.
Tests then read like English: `await loginPage.login(creds.email, creds.password);`. The page object encapsulates selectors; the fixture handles wiring; the test asserts behaviour.
Worker-scoped fixtures
By default fixtures are per-test. Mark a fixture as `{ scope: 'worker' }` and it is created once per parallel worker — perfect for expensive setup like spinning up a database, seeding a tenant, or fetching an OAuth token.