Skip to main content
QATraining
Back to curriculum
Chapter 3 of 7

Fixtures & Test Organisation

Use fixtures to compose reusable setup, isolate test state, and apply the Page Object Model without over-engineering.

14 min guide5 reference questions folded into the guide material
Code samples in
Guided briefing

Fixtures & Test Organisation video demo

A practical screen-share style walkthrough for chapter 3, showing how the Playwright idea works in TypeScript and Java.

Briefing focus

Fixtures — the composition primitive

This is a structured lesson briefing. Real video/audio can be added later as a media source.

Estimated time

10 min

  1. 1Fixtures — the composition primitive
  2. 2Page Object Model — used well

Transcript brief

Use fixtures to compose reusable setup, isolate test state, and apply the Page Object Model without over-engineering. The video brief explains the mental model first, then demonstrates the workflow using the course code samples, and finishes with reliability checks you can apply in CI.

Key takeaways

  • Translate the concept into a maintainable Playwright test.
  • Understand the TypeScript and Java equivalents without changing the test intent.
  • Avoid the common source of flaky or slow end-to-end tests for this topic.

Fixtures — the composition primitive

A fixture is a named resource with setup and teardown. Playwright's built-in fixtures — page, context, browser, request — are already injected into every test. You compose your own on top.

worker scope — created once per worker processe.g. DB seed, auth token, API clienttest scope · Test 1page, context, browser stateisolated per testtest scope · Test 2page, context, browser stateisolated per testtest scope · Test 3page, context, browser stateisolated per test
Fixtures have two scopes. `worker` fixtures live for the duration of a worker; `test` fixtures are fresh per test.
// tests/fixtures.ts
import { test as base, expect } from '@playwright/test';

type Fixtures = {
  signedInPage: import('@playwright/test').Page;
};

export const test = base.extend<Fixtures>({
  signedInPage: async ({ page }, use) => {
    await page.goto('/sign-in');
    await page.getByLabel('Email').fill(process.env.TEST_EMAIL!);
    await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
    await page.getByRole('button', { name: /sign in/i }).click();
    await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
    await use(page);
    // no teardown needed — context is disposed automatically
  },
});
export { expect };
Create a custom fixture that signs a user in once per test.

Page Object Model — used well

POM is not about wrapping every selector in a class. Use it to encapsulate flows that change — sign-in, checkout, filtering — and leave trivial pages as inline locators.

// pages/sign-in.page.ts
import { type Page, expect } from '@playwright/test';

export class SignInPage {
  constructor(private page: Page) {}

  async signIn(email: string, password: string) {
    await this.page.goto('/sign-in');
    await this.page.getByLabel('Email').fill(email);
    await this.page.getByLabel('Password').fill(password);
    await this.page.getByRole('button', { name: /sign in/i }).click();
    await expect(this.page).toHaveURL(/\/dashboard/);
  }
}
A minimal page object — encapsulates intent, not markup.

Structure by feature, not by page

Organise folders as tests/checkout/, tests/auth/, etc. It makes it obvious which feature owns which tests — essential when the suite passes 500 tests.