top of page

Welcome
to NumpyNinja Blogs

NumpyNinja: Blogs. Demystifying Tech,

One Blog at a Time.
Millions of views. 

Playwright Fixtures + BDD : Patterns That Actually Work

Jun 5
4 min read

When I first started writing BDD tests with Playwright, my step definitions were a mess. Every single step that needed a page object had to instantiate the object every time. If you have 15 step files where the object is needed then you get a nightmare instantiating the object in every page. Fixtures fixed that for me and once I understood the patterns, everything clicked.


Here is what I learned.


What even is a fixture?

Think of a restaurant table. Before customers arrive, the staff cleans the table, places the plates and the water. When customer arrives and sit down, they don’t do any of that, they just order their food and eat.


That’s exactly what a fixture does. It prepares everything before your test runs, so your test(the customer) can just walk in and do its job. No noise, no repetition, just the behaviour being tested.


The built-in page object you use in every test, thats a fixture. You are just creating your own on top of it.


The Nightmare Before Fixtures

To understand the importance of fixture, let us look at what happens when you don’t use them.


{Given, When, Then} from "playwright-bdd";

import { LoginPage } from "../pages/LoginPage.js";

import { DashboardPage } from "../pages/DashboardPage.js"

Given ("user is on login page", async ({page}) => {

const loginPage = new LoginPage(page); // Manual instantiation

await page.goto("https://www.example.com/login");

});

When ("user enters valid credentials", async({page}) =>{

const loginPage = new LoginPage(page); // Manual instantiation again

await loginpage.enterUserName("abc");

await loginpage.enterPassword("abc@123");

Then("user should see the dashboard page", async({page}) => {

const dashboardPage = new DashboardPage(page);//Another manual instantiation

await expect(dashboardPage.title).toBeVisible();

})

})

Imagine modifying constructor on the LoginPage in the future. If you have used the LoginPages in 15 different files, you now have to update all those files and the places you have created the object. It is an absolute nightmare.


Writing Your First Custom Fixture

Instead of creating page object inside the steps, we create a single file and centralize them.

//fixtures/fixtures.js

import { test as base } from "playwright-bdd";
import { LoginPage } from "../pages/LoginPage.js";
import { RegistrationPage } from "../pages/RegistrationPage.js";
import { DashboardPage } from "../pages/DashboardPage.js";

export const test = base.extend({
  loginPage: async ({ page }, use) => {
    //setup Phase
    await page.goto("www.example.com/login");
    await use(new LoginPage(page));
    //Teardown Phase
  },
  registrationPage: async ({ page }, use) => {
    await use(new RegistrationPage(page));
  },
  dashboardPage: async ({ page }, use) => {
    await use(new DashboardPage(page));
  },
});

The rule I always keep in mind is everything that runs before use() is  setup and everything after is teardown. If you need to close the connections or clean up, then that code goes after the use() call.


Connecting fixtures to your BDD steps

This is the part that confused me for  a long time. In plain playwright, you just use the test directly. However, in playwright-bdd, you do not just import Given, When, and Then directly from the library, you also need to pass your extended test object into createBdd() as playwright-bdd needs to know which Playwright test instance and fixtures your step definition should use. Else, your custom fixtures won’t be available inside your step file.


When you write:

import {createBdd} from “playwright-bdd”;
import {test} from “../fixtures/fixtures.js”;

export const {Given, When, Then} = createBdd(test);

You are basically telling playwright-bdd to use your specific test object and all the fixtures attached to it.


For example, if your fixture file contains:

export const test = base.extend({
loginPage: async ({page}, use) =>{
await use(new LoginPage(page));
},
})

Then in your step definition file, you can request the fixtures by name

Given("user is on login page", async ({loginPage})=> {
await loginPage.enterUsername("admin");
})

Here loginPage is available because createBdd(test) was given the extended test object. So, the purpose of createBdd(test) is to bind your BDD steps to your Playwright test context and fixtures.


Multi-Page Journeys Made Simple

Real user workflows often move across multiple pages. With Playwright fixtures, each step can request the page object it needs, and Playwright provides it automatically. There is no need to manually pass page objects or store them in shared variables between steps.

When("the user submits the registration form", async ({ registrationPage }) => {
await registrationPage.submit();
});
Then("they should see the welcome dashboard", async ({ dashboardPage }) => {
await expect(dashboardPage.title).toBeVisible();
});

In the above example, We notice no manual instantiation of objects between steps. The When step uses registrationPage, while the Then step uses dashboardPage. Each step simply asks for the fixture it needs, making the scenario easier to read and maintain. Playwright handles the setup and lifecycle of these page objects behind the scenes.


A Fixture for Teardown

loginPage: async ({ page }, use) => {
await page.goto("/login");
await use(new LoginPage(page));
}

Here the setup navigates to /login and creates LoginPage. use() passes the LoginPage instance to any test or step that requests the loginPage fixture. After the test completes, Playwright returns from await use() and can run any cleanup code. Without calling use, the fixture would never be provided to the test.


Putting It All Together

Here is a quick checklist for a fixture-based BDD setup:

  • Define your page objects as fixtures in a single shared page as fixtures.js file

  • Pass your extended test object to createBdd() at the top of each step definition file

  • Let each step request only the fixture it actually needs

  • Add all cleanup logic after use()

  • Compose fixture freely. You can request for fixture in any order and and it will provide you the result in the right order.

 
 

+1 (302) 200-8320

NumPy_Ninja_Logo (1).png

Numpy Ninja Inc. 8 The Grn Ste A Dover, DE 19901

© Copyright 2025 by Numpy Ninja Inc.

  • Twitter
  • LinkedIn
bottom of page