QA Engineer Skills 2026QA-2026Fixtures and Test Data

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
  • storageState lets 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