Keeping your Page Objects DRY with Composition
When writing automated test scripts, it’s easy to duplicate code to the point it becomes unwieldy and hard to maintain. At Testery, we recommend breaking large Page Object classes down into smaller, independent, and re-usable bits of code that represent the different entities on the page. If you’ve had exposure to React.js, Angular.js, or Vue.js, you’ll probably think of the components developers create that, when put together, make up the pages of the application. By adopting that pattern, Page objects then become a collection of smaller ‘page components’, that can be reused by other page objects.
Leveraging this pattern allows you to adhere to two important principles in programming:
- Single Responsibility Principle (SRP). The SRP states that a class, in our case a page component, should only do one thing and have one reason to change. If the class ends up growing beyond that, then it should be broken down into smaller components.
- Don’t Repeat Yourself (DRY) principle The DRY principle is aimed at reducing code duplication, as codebases with duplicated code take more effort to maintain (i.e. it takes longer to update your codebase if you need to hunt down every place where the particular logic you want to change is present).
Adhering to these principles is key to writing scalable and maintainable automation frameworks. Codebases that do not adhere to the SRP and DRY principles, like the giant page object classes mentioned, can be fixed by moving the duplicate code into its own unit, in this case a page component class, and calling that unit from all of the places where it was originally used.
By following these principles and breaking your page objects into smaller components, you end up with page objects that are made up of those smaller components, with the methods on the page object classes delegating their work to the methods on the component classes as you’ll see on the code sample below using Webdriver.io and Typescript. This results in less duplication across your page objects, more reusable code, and a more maintainable framework.
abstract class AbstractComponent {
constructor(protected rootSelector: string) {}
public click(selector: string) {
$(this.rootSelector).$(selector).waitForClickable({timeout: 5000})
$(this.rootSelector).$(selector).click();
}
public enterText(selector: string, text: string) {
$(this.rootSelector).$(selector).clearValue();
$(this.rootSelector).$(selector).setValue(text);
}
public isDisplayed(): boolean {
return $(this.rootSelector).isDisplayed();
}
}
class SearchBarComponent extends AbstractComponent {
get inputField() { return 'input[name="q"]'}
get submitBtn() { return 'button[type="submit"]'}
constructor(rootSelector: string) {
super(rootSelector);
}
public search(keyword: string) {
this.enterText(this.inputField, keyword);
this.click(this.submitBtn);
}
}
class HomePage {
get searchBar(): SearchBarComponent { return new SearchBarComponent('div.#search-section') }
search(keyword: string) {
this.searchBar.search(keyword);
}
}
class ResultsPage {
get searchBar(): SearchBarComponent { return new SearchBarComponent('div.#search-section') }
search(keyword: string) {
this.searchBar.search(keyword);
}
}
// steps.ts file
const validCourse = "Design Patterns in TypeScript";
defineStep(/^I search for a course from home page$/, () => {
const homePage = new HomePage();
homePage.search(validCourse);
});
defineStep(/^I search for a course from results page$/, () => {
const resultsPage = new ResultsPage();
resultsPage.search(validCourse);
});