Page Object design pattern - improve test maintainability
Tags: webdev, testing, angular
Published: 2021-02-17 22:57:07

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

### Recommendation
> “If you have WebDriver APIs in your test methods, You’re Doing It Wrong” [@shs96c](https://twitter.com/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
* [https://www.selenium.dev/documentation/en/guidelines_and_recommendations/page_object_models/](https://www.selenium.dev/documentation/en/guidelines_and_recommendations/page_object_models/)
* [https://martinfowler.com/bliki/PageObject.html](https://martinfowler.com/bliki/PageObject.html)
* [https://webdriver.io/docs/pageobjects/](https://webdriver.io/docs/pageobjects/)
* [https://www.toptal.com/selenium/test-automation-in-selenium-using-page-object-model-and-page-factory](https://www.toptal.com/selenium/test-automation-in-selenium-using-page-object-model-and-page-factory)
* [https://www.browserstack.com/guide/page-object-model-in-selenium](https://www.browserstack.com/guide/page-object-model-in-selenium)
* [https://www.pluralsight.com/guides/getting-started-with-page-object-pattern-for-your-selenium-tests](https://www.pluralsight.com/guides/getting-started-with-page-object-pattern-for-your-selenium-tests)
* [https://justin.abrah.ms/python/selenium-page-object-pattern–the-key-to-maintainable-tests.html](https://justin.abrah.ms/python/selenium-page-object-pattern–the-key-to-maintainable-tests.html)


* <span>Photo by <a href="https://unsplash.com/@martinadams?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Martin Adams</a> on <a href="https://unsplash.com/s/photos/martins-creek?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></span>