sudolabs logo

9. 11. 2023

3 min read

Improving Playwright Testing with Fixtures and POMs

In Playwright testing, fixtures and Page Object Models (POMs) are invaluable for keeping code reusable and tests clean. In this article, we explore their practical use, emphasizing the creation of new POMs and fixtures to simplify your Playwright testing process.

Martin Naščák

Software Engineer

In our playwright tests, we are using fixtures and POMs to improve our developer experiences. POMs (Page Object Models) are used to abstract the page logic from the tests into a class-based method. Fixtures are used to set up helper methods or provide access to the POM instance for the test.

Fixtures and POMs help us to encapsulate reusable code. It helps us to keep our tests clean and maintainable. The rule of thumb? Whenever you can encapsulate code for broader use, create a new POM or fixture.

Fixtures

Test fixtures are used to establish an environment for each test, giving the test everything it needs and nothing else. Test fixtures are isolated between tests. With fixtures, you can group tests based on their meaning, instead of their common setup.

We are using fixtures to provide access to POMs.

We should never create an instance of POM inside the test. We should use fixtures to get access to POM.

How to create a fixture

To create a fixture, we need to create an object that implements methods that will be accessible inside the test.

// We define our type for Fixture
export type SessionFixture = {
bookSessionPage: BookSessionPOM
upcomingSessionsListPage: UpcomingSessionsListPOM
fillCreditCard: () => void
}
// We create our object that has methods that we want to use in our test.
// Calling `use` method will create an instance of the object and pass it to the test.
// Example of session fixture
export const sessionFixture: SessionFixture = {
bookSessionPage: async ({ page }, use) => {
await use(new BookSessionPage(page)); // BookingSessionPage is a POM
},
upcomingSessionsListPage: async ({ page }, use) => {
await use(new UpcomingSessionsListPage(page)); // UpcomingSessionListPage is a POM
},
// Helper method that does not relies on POM
fillCreditCard: async ({ page }: { page: Page }, use) => {
const fillCreditCard = async () => {
await page
.getByPlaceholder('Card number')
.fill('4444444444444444')
await page
.getByPlaceholder('MM / YY')
.fill('444')
await page
.getByPlaceholder('CVC')
.fill('444')
}
await use(fillCreditCard)
},
};
// We export a test that extends our fixture. We can import it inside the test to use it with a SessionFixture
// However there is another way to use fixtures, without extending this exact test.
export const test = base.extend<SessionFixture>(sessionFixture);

In this example we can see, fillCreditCardis not a POM, it is a helper method that just does one thing. POM is a class that contains multiple methods, we will talk about it later. bookSessionPage and upcomingSessionsListPage returns an instance of POM.

How to use Fixtures in our test

In this example, we are using 2 fixtures together in one test. We are not importing test from sessionFixture but we are importing sessionFixture and paymentFixture and extending them together.

We can also import test from sessionFixture and use it if we do not need any other fixtures.

import { test as base } from '@playwright/test'
const test = base.extend<PaymentFixture & SessionFixture>({
...paymentFixture,
...sessionFixture,
})
// Or import from our fixture
// import { test } from "fixtures/sessionFixture"
test.describe('Your test', () => {
test(
'Your test case',
async ({
page,
isMobile,
fillCreditCard, // Provided by sessionFixture
bookSessionPage, // Provided by sessionFixture
upcomingSessionsListPage, // Provided by sessionFixture
}) => {
...
// Example
await bookSessionPage.run(
{ isMobile },
{
...
}
)
await fillCreditCard();
...
}
}

Page Object Models (POMs)

The page object model is a design pattern that helps us to abstract the page logic from the tests. This helps us to keep our tests clean and maintainable. POM is a class-based object that represents a page. It contains all the methods that are used to interact with the page.

How to create POMs

To create POM, we need to create an interface that we will implement in our POM class. An interface contains methods that we will implement.

In our constructor, we allocate all our selectors. Let's consider the following example:

export interface BookSessionPOM {
run: PageAction
goTo: PageAction
submitSession: PageAction
fillForm: PageAction
}
export class BookSessionPage implements BookSessionPOM {
readonly page: Page
readonly bookSessionLink: Locator
readonly emailInput: Locator
readonly nameInput: Locator
readonly informationInput: Locator
readonly submitButton: Locator
... // And more selectors if required
constructor(page: Page) {
this.page = page
this.bookSessionLink = page.locator(/* ... */)
this.emailInput = page.locator(/* ... */)
this.nameInput = page.locator(/* ... */)
this.informationInput = page.locator(/* ... */)
this.submitButton = page.locator(/* ... */)
... // And more selectors if required
}
// Now we implement methods from our type `Auth`
// Methods should be simple and do exactly one thing or one action
goTo: PageAction = async () => {
await this.page.goto(HOMEPAGE)
await expect(this.bookSessionLink).toBeVisible()
await this.bookSessionLink.click()
}
fillForm: PageAction = async () => {
await this.emailInput.fill(/* ... */)
await this.nameInput.fill(/* ... */)
await this.informationInput.fill(/* ... */)
}
submitSession: PageAction = async () => {
await this.submitButton.click()
}
run: PageAction = async () => {
await this.goTo()
await this.fillForm()
await this.submitSession()
}
}

How to use POMs in our test

To use the POM, you should create a fixture that will initialize an instance and return it by using use. This way you can reliably use POMs that are already instantiated in the fixture.

Do not create an instance of POMs inside the test. We should use fixtures to get access to POMs.

We already explained how to use fixtures and how to return access to a POM. Here’s an example:

type ClientFixture = {
signInClientPage: SignInClientPage
}
export const clientFixture = {
signInClientPage: async ({ page }, use) => {
await use(new SignInClientPage(page))
},
...
}

We create a fixture clientFixture that has the function signInClientPage. In this function, we will return SignInClientPage POM with use param. Then we can extend the test with the fixture clientFixture and use the POM inside.

Here is an example:

import { test as base } from '@playwright/test'
// Use object spreading for multiple fixtures.
const test = base.extend<ClientFixture>(clientFixture)
test.describe('Your test', () => {
test(
'Your test case',
async ({
page,
signInClientPage,
}) => {
...
// Example
await signInClientPage.signIn(...)
...
}
}

The key takeaway is simple

Whenever you can encapsulate code for broader use, create a new POM or fixture. This approach empowers you to master Playwright testing, making your development experiences smoother and your tests more effective.

Share

Let's start a partnership together.

Let's talk

Our basecamp

700 N San Vicente Blvd, Los Angeles, CA 90069

Follow us


© 2023 Sudolabs

Privacy policy
Footer Logo

We use cookies to optimize your website experience. Do you consent to these cookies and processing of personal data ?