Playwright Fixtures Deep Dive — Test Setup Done Right (2026)
Master Playwright fixtures: worker-scoped auth, per-test data seeding, custom pages, and the fixture composition patterns that eliminate beforeEach boilerplate.

Last updated 2026-07-20 · 11 min read · By Avinash K
beforeEach hooks are Playwright's biggest smell. Fixtures replace them with typed, composable, worker-scoped setup that runs faster and reads cleaner. Once your team internalizes fixtures, PR reviews get 30% shorter because setup is centralized.
Key takeaways
- The 3 fixture scopes (test, worker, project) and when to use each.
- Worker-scoped auth: sign in once per worker, save 90% of setup time.
- Fixture composition — extend from base to feature suites.
- Fixture teardown for cleanup that always runs.
1. Three fixture scopes
| Scope | Lifetime | Use for |
|---|---|---|
| test | per test | fresh data, page objects |
| worker | per worker process | auth state, DB pools, browser context |
| project | per test project | shared config, feature flags |
2. A custom fixture from scratch
// fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
type MyFixtures = { loginPage: LoginPage; seededOrder: { id: string } };
export const test = base.extend<MyFixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
seededOrder: async ({ request }, use) => {
const order = await request.post('/api/orders', { data: { total: 100 } });
await use(await order.json());
await request.delete('/api/orders/' + (await order.json()).id);
},
});
export { expect } from '@playwright/test';Now every test that needs a seeded order gets one — and cleanup runs automatically after the test.
3. Worker-scoped auth — the 10x speedup
// auth.setup.ts — runs once, produces storageState.json
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.TEST_USER);
await page.getByLabel('Password').fill(process.env.TEST_PASS);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.context().storageState({ path: 'auth.json' });
});
// playwright.config.ts
projects: [
{ name: 'setup', testMatch: /auth\.setup\.ts/ },
{
name: 'chromium',
use: { storageState: 'auth.json' },
dependencies: ['setup'],
},
];A 500-test suite that used to log in 500 times now logs in once per worker. Real numbers from our stack: 42 min → 8 min. See the official auth guide.
4. Composing fixtures across suites
Build a base fixture with common setup, then extend per feature: const paymentTest = test.extend({ stripeMock: ... }). Compose don't duplicate. See our POM complete guide for how fixtures + POM together replace 90% of beforeEach.
5. Teardown that always runs
Everything after await use(value) is teardown. Even if the test fails, it runs. Use this for DB cleanup, feature-flag reset, and file deletes. Cross-reference the Playwright interview questions hub — fixture teardown is asked at 60% of senior SDET interviews.