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 Fixtureexport 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 fixtureexport 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.
You might
also like