Fixtures and Test Data
Playwright fixtures are a dependency injection system that lets you define reusable setup/teardown logic — authenticated sessions, test data, page objects — and inject them into tests declaratively. Combined with proper test data management, fixtures eliminate boilerplate and make tests self-contained.
Built-in Fixtures
Every @playwright/test test receives built-in fixtures automatically:
| Fixture | Description |
|---|---|
page |
A new page in a fresh browser context |
context |
A browser context (owns the page) |
browser |
The shared browser instance |
request |
An API request context for direct HTTP calls |
browserName |
Current browser name (chromium, firefox, webkit) |
// `page` and `context` are fresh for every test — no shared state
test('example', async ({ page, context, request }) => {
// page: isolated browser tab
// context: isolated browser session
// request: make API calls without a browser
});
Custom Fixtures
Custom fixtures let you extend the test function with your own dependencies:
// fixtures/auth.ts
import { test as base, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
import { DashboardPage } from '../pages/dashboard.page';
type MyFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
authenticatedPage: DashboardPage;
};
export const test = base.extend<MyFixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await use(loginPage);
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
authenticatedPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('test@example.com', 'password');
await use(new DashboardPage(page));
},
});
export { expect };
Using Custom Fixtures
// tests/dashboard.spec.ts
import { test, expect } from '../fixtures/auth';
// Receives an already-authenticated dashboard page
test('dashboard shows user name', async ({ authenticatedPage }) => {
await expect(authenticatedPage.userName).toHaveText('Test User');
});
// Receives just the login page
test('invalid login shows error', async ({ loginPage }) => {
await loginPage.login('bad@test.com', 'wrong');
expect(await loginPage.getErrorMessage()).toContain('Invalid');
});
Authentication State Reuse
Logging in through the UI for every test is slow. Playwright's storageState lets you authenticate once and reuse the session across tests.
// auth.setup.ts — runs once before all tests
import { test as setup } from '@playwright/test';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
// Save authentication state (cookies + localStorage)
await page.context().storageState({ path: '.auth/user.json' });
});
// playwright.config.ts
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'tests',
dependencies: ['setup'],
use: { storageState: '.auth/user.json' },
},
],
});
Now all tests in the tests project start already logged in — no login UI interaction per test.
Test Data Management
API-Based Setup (Recommended)
Create test data through the API before UI tests — faster and more reliable than UI-based setup:
test.beforeEach(async ({ request }) => {
// Create test data via API
await request.post('/api/products', {
data: { name: 'Test Product', price: 29.99 },
});
});
test('product appears in catalog', async ({ page }) => {
await page.goto('/products');
await expect(page.getByText('Test Product')).toBeVisible();
});
test.afterEach(async ({ request }) => {
// Clean up via API
await request.delete('/api/products/test-product');
});
Fixture-Based Test Data
type TestData = {
testUser: { email: string; password: string };
testProduct: { name: string; price: number };
};
export const test = base.extend<TestData>({
testUser: async ({}, use) => {
await use({ email: 'test@example.com', password: 'password' });
},
testProduct: async ({ request }, use) => {
// Create product via API
const response = await request.post('/api/products', {
data: { name: `Product-${Date.now()}`, price: 29.99 },
});
const product = await response.json();
await use(product);
// Teardown: clean up after test
await request.delete(`/api/products/${product.id}`);
},
});
Key Takeaways
- Built-in fixtures (
page,context,request) provide isolated, fresh environments per test - Custom fixtures inject reusable setup (page objects, authenticated sessions, test data) into tests
storageStatelets you authenticate once and reuse sessions — eliminating per-test login overhead- Create test data via API in fixtures for speed and reliability — not through the UI
- Fixtures provide automatic teardown — the code after
await use()runs after the test completes