Skip to content
SDETRole
SDETRole

Playwright interview question

How do Playwright fixtures work? Build a custom fixture with test.extend.

intermediate 2–5 yrs #fixtures#test-extend#page-object-model#worker-scope

Short answer

Fixtures are on-demand setup and teardown units injected by name into tests. Create them with test.extend: code before await use(value) is setup, after it is teardown. They can be test- or worker-scoped, depend on other fixtures, run automatically with auto: true, and replace beforeEach hooks and base classes.

Answer

A fixture is a named piece of setup and teardown that Playwright Test creates only when a test asks for it, by destructuring it from the test’s first argument. page, context, browser and request are built-in fixtures. You define your own with test.extend, which returns a new test function that knows about them. Fixtures replace most beforeEach/afterEach hooks and base-class inheritance, with better encapsulation and reuse.

Anatomy of a fixture

// fixtures.ts
import { test as base, expect, type Page } from '@playwright/test';

class CartPage {
  constructor(private page: Page) {}
  async open() { await this.page.goto('/cart'); }
  items() { return this.page.getByRole('listitem'); }
}

type TestFixtures = {
  cartPage: CartPage;
  seededProduct: { id: string; name: string };
};

export const test = base.extend<TestFixtures>({
  cartPage: async ({ page }, use) => {
    await use(new CartPage(page));
  },

  seededProduct: async ({ request }, use) => {
    // setup
    const res = await request.post('/api/products', { data: { name: `Lamp ${Date.now()}` } });
    expect(res.ok()).toBeTruthy();
    const product = await res.json();

    await use(product);          // the test runs here

    // teardown: runs even if the test failed
    await request.delete(`/api/products/${product.id}`);
  },
});

export { expect };
// cart.spec.ts
import { test, expect } from './fixtures';

test('added product shows in cart', async ({ page, cartPage, seededProduct }) => {
  await page.goto(`/products/${seededProduct.id}`);
  await page.getByRole('button', { name: 'Add to cart' }).click();
  await cartPage.open();
  await expect(cartPage.items()).toHaveText([seededProduct.name]);
});

Key mechanics:

  • Everything before await use(value) is setup; everything after is teardown.
  • Fixtures can depend on other fixtures (seededProduct uses request). Playwright builds the dependency graph and tears down in reverse order.
  • A test that does not mention seededProduct never creates it. Hooks, by contrast, run for every test in scope.

Worker-scoped fixtures

Expensive resources that can be shared across tests in one worker process use { scope: 'worker' }:

type WorkerFixtures = { workerAccount: { email: string; password: string } };

export const test = base.extend<{}, WorkerFixtures>({
  workerAccount: [async ({}, use, workerInfo) => {
    const email = `qa+w${workerInfo.workerIndex}-${Date.now()}@example.com`;
    // create the account once per worker via API...
    await use({ email, password: 'S3cure!pass' });
    // delete it when the worker shuts down
  }, { scope: 'worker' }],
});

Worker fixtures are typed in the second generic parameter of extend. Using workerIndex keeps data unique across parallel workers.

Other options

  • { auto: true }: runs for every test even if not requested, useful for attaching logs on failure or failing on console errors.
  • Option fixtures ([defaultValue, { option: true }]) can be overridden per project in the config or with test.use().
  • mergeTests(a, b) combines fixture sets from different modules.
  • { box: true } hides a helper fixture’s steps from the report.

Why fixtures beat hooks and base classes

  • Explicit dependencies: the test signature shows what it needs.
  • Encapsulated teardown next to setup, so cleanup is not forgotten.
  • Lazy: no wasted setup for tests that do not need it.
  • Composable: page objects, API clients, test data and auth can be layered without deep inheritance.

What interviewers look for

  • Correct test.extend syntax with use() and teardown.
  • Test versus worker scope, and typing via the generics.
  • Understanding that fixtures are lazy and composable.
  • Real uses: page objects, seeded data with cleanup, per-worker accounts.

A strong answer covers

  • Defines fixtures with test.extend and injects by name
  • Setup before use(), teardown after
  • Test vs worker scope
  • Lazy: only fixtures a test requests are created; fixtures can depend on each other
  • Advantages over beforeEach/base classes; auto fixtures, mergeTests, page objects
Practise answering under time pressure

Type to search across SDETRole.

Full-text search by Pagefind · runs in your browser Open search page →