Photo by Martin Adams on Unsplash

Delivering high quality applications is only/somewhat possible when following best software engineering practices. Automated testing is one of them. When implemented advisedly, automated testing provides a great return of investment in terms of less time spent on fixing incorrect behavior and more time left for creating a new business value.

Page Object (PO) pattern abstracts access to the page elements from actual tests and makes automated tests more maintainable.

Table of Content

The need for Page Object

Coupling access to page elements (e.g. input field, submit button) and actual test code (e.g. verification and assertion) leads to hard to maintain UI tests.

Consider following test that validates input field gets cleaned after the submit button is clicked

// app.e2e-spec.ts
import { by, element } from 'protractor';

describe('PageObject example usage', () => {

  it('should clear input field after submit', async () => {

    expect(await element(by.id('input-field-id')).getText()).toEqual('');
    await element(by.id('input-field-id')).sendKeys('apple')
    expect(await element(by.id('input-field-id')).getText()).toEqual('apple');
  
    await element(by.id('submit-button-id')).click();
  
    expect(await element(by.id('input-field-id')).getText()).toEqual('');
  });
  
});

To get input-field-id field text value, we are duplicating
element(by.id('input-field-id')).getText() three times and to set it’s value we need to access it once again by element(by.id('input-field-id')).sendKeys('apple')

Having to rename input-field-id even in this simple example will cause a code change in four different places. Four! Consider maintenance cost of twenty tests where input-field-id is used ten times in each.

NOTE: Field ID rename is just an example given to emphasis the importance of PO. Any change in component that results in a change of how we access the component has the same problem.

Page Object to the rescue

Page Object is a design pattern which enables a clean separation of code used to access a specific page and it’s elements, and the actual test code. It encapsulates the interactions with the UI component itself. At the same time it provides an API that serves as a single entry point to the specific page. The benefit is reduced code duplication and improved maintainability. Test logic now clearly represents the intention, and it’s not interlaced with the UI component access code.

Page Object can represent the whole page or a meaningful contextual part of the page (e.g. modal component). If it becomes too complex, it can be split up into multiple smaller PageComponentObjects (PCO).

PageComponentObject is a concept that represents smaller, discrete chunk of the page. It can be included in Page Object or nested inside another PageComponentObject.

Consider previous test example after applying the Page Object pattern

  • Define Page Object that provides specific page API
import {by, element } from 'protractor';

// app.po.ts
export class AppPage {

  async getInputFieldText(): Promise<string> {
    return element(by.id('input-field-id')).getText();
  }

  async setInputFieldText(value: string): Promise<void> {
    return element(by.id('input-field-id')).sendKeys(value);
  }

  async clickSubmitButton(): Promise<void> {
    return element(by.id('submit-button-id')).click();
  }
}
  • Update test to use Page Object to access page elements
// app.e2e-spec.ts
import { AppPage } from './app.po';

describe('PageObject example usage', () => {
  let page: AppPage;

  beforeEach(() => {
    page = new AppPage();
  });

  it('should clear input field after submit', async () => {

    expect(await page.getInputFieldText()).toEqual('');
    await page.setInputFieldText('apple')
    expect(await page.getInputFieldText()).toEqual('apple');

    await page.clickSubmitButton();

    expect(await page.getInputFieldText()).toEqual('');
  });
});

In contrast to the test example without applied Page Object pattern, having to rename input-field-id in case where twenty tests use input-field-id now requires two changes in the class that encapsulates our page.

Recommendation

“If you have WebDriver APIs in your test methods, You’re Doing It Wrong”

@shs96c
  • Name your Page Objects with the po.ts suffix so they are easily recognizable
  • Do not make verification and assertions inside Page Object
    • only exception is verification that the page is loaded and ready to be used
  • when navigating between pages, return the Page Object for the new page

Conclusion

The biggest advantage of UI test is that it’s the most accurate simulation of an actual user’s experience. But if not implemented advisedly it can be very brittle and hard to maintain.

Page Object usage improves test structure, intention and maintainability by separating page information from the test and providing an API that server as a single entry point to the page.

Sources