Page Objects

The page object pattern is a classic pattern to model application state (pages) and things you do (or an automated test tool does) to them.

There’s often negativity towards best practices, but I can confidently say that page objects are a best practice for test automation of UI systems, which isn’t saying they will be exactly the same in every context, but there’s a common best practice pattern there which you can use.

Page objects, as a pattern, typically:

  1. Inherit from a base page object/container which stores common actions like:
    • instantiating the object looking for a known element that defines that page’s existence
    • optionally allow a ‘visit’ to the page during instantiation using some defined URL/path
    • provides actions and properties common to all pages, for example: waiting for the page, checking the page is displayed, getting the title/url of the page, and checking cookies and local storage items for that page;
  2. Define actions as methods which are ways of interacting with that page (such as logging in);
  3. Do not expose internals about the page outside the page – for example they typically don’t expose elements or element selectors which should only be used within actions/methods for that page which are exposed; and
  4. Can also be modeled as components for user interfaces that are built using components to give greater re-usability of the same components across different pages.

The biggest benefit I have found from using page objects as a pattern is having more deterministic end-to-end tests since instantiating a page means you know you are on that page, so the automated tests will fail more reliably with a better understanding of what went wrong.

Further reading:

3 replies on “Page Objects”

Hey Alister, Cool blogpost!

I personally like to prefer a more ‘functional programming’ kind of page object pattern like below.

“`
import { expect, Page } from ‘@playwright/test’

//Selectors

const conversionBtn = ‘#convert_btn’
const convertFrom = ‘#from_currency’
const convertTo = ‘#to_currency’
const conversionValue = ‘#base_amount’
const conversionMsg = ‘.conversion-response’

//Actions
export const enterConversionValue = async (page: Page, value: string) => {
await page.type(conversionValue, value)
}

export const selectFromCurrency = async (page: Page, currency: string) => {
await page.locator(convertFrom).selectOption(currency)
}

export const selectToCurrency = async (page: Page, currency: string) => {
await page.locator(convertTo).selectOption(currency)
}

export const convertCurrency = async (page: Page) => {
await page.click(conversionBtn)
}

// Assertions

export const confirmConversionMessage = async (page: Page, message: string) => {
await expect(page.locator(conversionMsg)).toContainText(message)
}
“`

And the test will look like

“`
import test from ‘@playwright/test’
import * as homePage from ‘../actions/ui/homePage.ui.actions.functional’

test(‘Should be able to convert currency’, async ({ page }) => {
await page.goto(‘https://cash-conversion.dev-tester.com/’)
await homePage.enterConversionValue(page, ‘100’)
await homePage.selectFromCurrency(page, ‘AED’)
await homePage.selectToCurrency(page, ‘EUR’)
await homePage.convertCurrency(page)
await homePage.confirmConversionMessage(page, ‘100 United Arab Emirates Dirham is about 25.23 Euro’)
})
“`
Compared to more ‘traditional’ Page Object pattern like

“`
import { expect, Locator, Page, request } from ‘@playwright/test’

export class HomePage {
private readonly page: Page
private readonly conversionBtn: Locator
private readonly convertFrom: Locator
private readonly convertTo: Locator
private readonly conversionValue: Locator
private readonly conversionMsg: Locator

constructor(page: Page) {
this.page = page
this.conversionBtn = page.locator(‘#convert_btn’)
this.convertFrom = page.locator(‘#from_currency’)
this.convertTo = page.locator(‘#to_currency’)
this.conversionValue = page.locator(‘#base_amount’)
this.conversionMsg = page.locator(‘.conversion-response’)
}

//Actions

async enterConversionValue(value: string) {
await this.conversionValue.type(value)
}

async selectFromCurrency(currency: string) {
await this.convertFrom.selectOption(currency)
}

async selectToCurrency(currency: string) {
await this.convertTo.selectOption(currency)
}

async convertCurrency() {
await this.conversionBtn.click()
}

// Assertions

async confirmConversionMessage(message: string) {
await expect(this.conversionMsg).toContainText(message)
}
}
“`

With its test like

“`
import test from ‘@playwright/test’
import { HomePage } from ‘../actions/ui/homePage.ui.actions.pageObject’

test(‘Should be able to convert currency’, async ({ page }) => {
await page.goto(‘https://cash-conversion.dev-tester.com/’)
const homePage = new HomePage(page)
await homePage.enterConversionValue(‘100’)
await homePage.selectFromCurrency(‘AED’)
await homePage.selectToCurrency(‘EUR’)
await homePage.convertCurrency()
await homePage.confirmConversionMessage(‘100 United Arab Emirates Dirham is about 25.23 Euro’)
})
“`

I feel that the ‘functional programming’ style approach is cleaner and more simple to read and understand compared to the traditional approach of page objects with classes and its constructors. What are your thoughts on this?

Leave a Reply

Your email address will not be published. Required fields are marked *