# 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.

- URL: https://sudolabs.com/insights/improving-playwright-testing-with-fixtures-and-poms
- Published: 2023-11-09
- Updated: 2024-01-17
- Author: Martin Naščák, Software Engineer
- Categories: Tech
- Language: English

In our playwright tests, we are using [fixtures](https://playwright.dev/docs/test-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.

```typescript
// 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.

```typescript
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**](https://playwright.dev/docs/pom) 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:

```typescript
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:

```typescript
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:

```typescript
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.
