DEV Community

Cover image for Page Object design pattern - improve test maintainability
b0r
b0r

Posted on • Updated on • Originally published at allthingsangular.com

Page Object design pattern - improve test maintainability

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.

Content

  • The need for Page Object
  • Page Object to the rescue
  • Conclusion

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('');
  });

});
Enter fullscreen mode Exit fullscreen mode

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 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();
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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('');
  });
});
Enter fullscreen mode Exit fullscreen mode

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 one class only.

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

Top comments (1)

Collapse
 
kailashpathak7 profile image
Kailash P.

In the majority of cases, we prefer to build our automation framework using the Page Object Model (POM) design pattern.

Now, imagine having a 🔨 tool that automatically records and generates
POM(page classes) for us. How much easier would our work become?

⚙ Applitools TestGenAI for Cypress provides us this capability.

applitools.com/blog/transform-user...

🔍 Applitools TestGenAI for Cypress empowers users to quickly create robust, auto healing automated tests that can validate even the most complex scenarios within seconds.