Sitemap

Playwright Test Fixtures Explained: Customizing Test Lifecycle

4 min readJul 9, 2025
Press enter or click to view image in full size

In our last blog, we explored how Playwright handles API mocking and network interception, enabling teams to simulate complex network conditions with precision. That article emphasized how Playwright goes beyond UI testing and allows control over backend communication, helping developers and testers to stabilize and isolate their test environments.

Building on that foundation, let’s now talk about one of Playwright’s most powerful and underrated features: test fixtures. If you’ve worked with test frameworks like Jest, Mocha, or Cypress, you’re probably familiar with hooks like beforeEach and afterEach. But Playwright's approach to fixtures is significantly more structured and modular.

This post explains the Playwright test fixtures system, including how to customize the test lifecycle using test.describe, beforeEach, use, and shared context patterns. We’ll walk through core concepts, practical examples, and best practices to help you master fixture-driven testing.

What Are Playwright Fixtures?

In Playwright Test, fixtures are reusable building blocks used to set up and tear down resources needed for test execution. These can include:

  • Browser and context management
  • Page setup
  • Authenticated sessions
  • Mock data or API responses
  • Any custom setup logic required per test or per test suite

The key value of fixtures in Playwright is that they enable cleaner test code, shared context, and parallel execution, without manual lifecycle management.

Basic Fixture Lifecycle in Playwright

A Playwright test runs in a tightly controlled lifecycle powered by fixtures:

  1. Global Setup (optional via global-setup)
  2. Project-specific Setup
  3. Test-scoped Setup (beforeEach, afterEach)
  4. Suite-scoped Setup (test.describe)
  5. Teardown

Here’s a quick illustration:

import { test, expect } from ‘@playwright/test’;

test.beforeEach(async ({ page }) => {
await page.goto(‘https://example.com');
});

test(‘Page title should be correct’, async ({ page }) => {
await expect(page).toHaveTitle(‘Example Domain’);
});

In the above example, page is a built-in fixture provided by Playwright.

Custom Fixtures: When and Why?

While built-in fixtures like browser, context, and page are sufficient for many tests, real-world applications often require custom setup:

  • Setting cookies or tokens for login
  • Loading test data before the test
  • Managing database or API mocks
  • Injecting feature flags or configurations

You can define custom fixtures using the test.extend method:

import { test as base } from ‘@playwright/test’;

const test = base.extend<{
authToken: string;
}>({
authToken: async ({}, use) => {
const token = await fetchTokenFromAPI();
await use(token);
},
});

test(‘Authenticated API call’, async ({ request, authToken }) => {
const res = await request.get(‘/secure-endpoint’, {
headers: { Authorization: `Bearer ${authToken}` }
});
expect(res.ok()).toBeTruthy();
});

Key Takeaway: Fixtures help you extract shared setup logic and pass it around as test context.

Using test.use() to Configure Per-Test Fixtures

test.use() allows you to override fixture values for individual tests or test suites.

test.use({
storageState: ‘logged-in.json’, // for authentication
viewport: { width: 1280, height: 720 },
});

test(‘Test with custom viewport’, async ({ page }) => {
await page.goto(‘/dashboard’);
// your test logic
});

This is especially useful when you want to run the same test under different conditions — like mobile vs desktop viewports, or logged-in vs guest users.

beforeEach, afterEach, and Lifecycle Hooks

Playwright supports common lifecycle hooks similar to Mocha or Jest:

test.beforeEach(async ({ page }) => {
await page.goto(‘/login’);
});

test.afterEach(async ({ page }) => {
// Optional cleanup
});

These are scoped per test and run before/after every test case in the current file or block.

You can also scope setup logic using test.describe():

test.describe(‘Admin Panel’, () => {
test.beforeEach(async ({ page }) => {
await page.goto(‘/admin’);
// assume admin login
});

test(‘Can see admin dashboard’, async ({ page }) => {
await expect(page.locator(‘h1’)).toHaveText(‘Admin Dashboard’);
});
});

This groups the tests and allows shared setup inside that block.

Sharing Context Between Tests

Sometimes, you need to share state across multiple tests, such as authenticated sessions or loaded data. While Playwright encourages isolated tests, you can share context using fixtures safely.

Here’s an example:

const test = base.extend<{
userSession: { token: string; userId: string };
}>({
userSession: async ({}, use) => {
const session = await loginAndGetSession();
await use(session);
},
});

test(‘User session is valid’, async ({ userSession }) => {
expect(userSession.token).toBeDefined();
});

This is cleaner and more maintainable than using global variables or external files.

Final Thoughts

Playwright’s fixture model enables a highly modular, configurable, and scalable approach to automated testing. Whether you’re working on UI automation, API testing, or hybrid setups, fixtures give you control over test environments without sacrificing speed or maintainability.

As a leading Automation Testing Company, at Testrig Technologies, we use Playwright extensively in client projects — often building complex fixture setups for SaaS platforms, fintech apps, and eCommerce portals. Our experience has shown that well-structured fixtures lead to better test stability and faster CI pipelines.

--

--

Testrig Technologies
Testrig Technologies

Written by Testrig Technologies

As an independent software testing company, we provide modern quality assurance and software testing services to global clients.