Combine Fixtures & Page Object Models for DRYer Test Code in Playwright

Share on social

Table of contents

If you're using Playwright for end-to-end testing or synthetic monitoring with Checkly, you've likely considered reusing your test code across different test cases. A common approach for this is using Page Object Models (POMs). However, if you're like me, you might have mixed feelings about POMs—while they help organize your code, they can sometimes feel cumbersome to set up and maintain.

But recently, I found that pairing POMs with Playwright fixtures can significantly improve the developer experience. It simplifies test code and reduces the boilerplate needed to initialize and use POMs. If that sounds intriguing, let's dive in.

What is a Page Object Model?

If you're new to Page Object Models, here's a quick example. A POM is essentially a class that represents a specific area of your application under test. Let’s take a look at a DashboardPage class for the Checkly web application:

dashboardPage.ts
class DashboardPage {
  readonly url: string;
  readonly page: Page;
  readonly $homeDashboard: Locator;

  constructor(page: Page) {
    this.url = '<https://app.checklyhq.com/dashboard>';
    this.page = page;
    this.$homeDashboard = page.locator('#home-dashboard');
  }

  async navigate() {
    await this.page.goto(this.url);
  }

  async isDashboardReady() {
    await this.$homeDashboard.waitFor();
  }
}

This DashboardPage class encapsulates the locators and methods related to the dashboard area of the Checkly app. By using POMs, you can keep your test cases clean and maintainable.

Here’s an example of how you might use this in a test case:

dashboardLoad.spec.ts
test('log into Checkly', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.navigate();
  await loginPage.login('user@example.com', 'password');

  const dashboardPage = new DashboardPage(page);
  await dashboardPage.isDashboardReady();
});

This approach is clear and readable, but it requires you to manually create instances of each POM and pass around the page object. This is where Playwright fixtures can come in handy.

The Downsides of Page Object Models

Two things always bugged me when using POMs:

  1. Reaching outside of test cases: Playwright offers tools to keep functionality self-contained within your tests, but POMs often require you to reference external objects, which can feel messy.
  2. Object initialization: Manually initializing objects for every POM can be repetitive and tedious, especially in larger test suites.

Wouldn't it be nice if we could simply declare that we want to use a LoginPage or DashboardPage within our test cases and have them magically available without all the constructor calls? With Playwright fixtures, this is entirely possible.

Implementing Playwright Fixtures with Page Object Models

Let’s see how we can achieve this by extending Playwright’s fixtures.

First, create a base.ts file where we'll extend Playwright’s test function:

base.ts
import { test as base, expect } from '@playwright/test';
import { LoginPage } from './LoginPage';
import { DashboardPage } from './DashboardPage';

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

export { expect };

In this file, we use Playwright’s extend function to create custom fixtures. These fixtures automatically instantiate our LoginPage and DashboardPage classes, so they’re readily available in our test cases without additional setup.

Now, update your test cases to use the new test object from base.ts:

dashboardLoad.spec.ts
import { test } from './base';

test('log into Checkly', async ({ loginPage, dashboardPage }) => {
  await loginPage.navigate();
  await loginPage.login('user@example.com', 'password');
  await dashboardPage.isDashboardReady();
});

Here’s what’s happening:

  • We’ve replaced the standard test import with our extended test from base.ts.
  • In the test function, loginPage and dashboardPage are now automatically available as fixtures.

This setup eliminates the need for repetitive constructor calls and keeps your test cases clean and concise.

Running the Tests

Let’s see this in action. Run your tests with:

npx playwright test --headed

You should see the test execute successfully, logging in and verifying the dashboard page, all with less boilerplate code.

See page object models with fixtures in action

Check out Stefan’s video on how to keep your code DRY with this method:

Conclusion

By combining Page Object Models with Playwright fixtures, you can streamline your test code and enhance the developer experience. This approach reduces repetition and makes your test cases more readable and maintainable. I’ve become a big fan of this method and will be incorporating more fixture-based POMs in my Playwright projects.

If you have any questions, feel free to ask in the comments or join our Checkly community Slack.

Share on social