Skip to main content
Version: 9.0.0

How to Build a Page Object Model

Understanding the Page Object Model

The Page Object Model (POM) is a powerful design pattern that enhances the clarity and maintainability of your automated tests. It enables you to abstract page-specific information away from your test scripts, making your code more organized and robust.

The POM approach involves encapsulating unique selectors and specific page-related instructions within a dedicated "page object." This abstraction allows your tests to remain functional even when your application's design evolves. The key benefits of POM include readability, code organization, and ease of maintenance.

Creating a Page Object Model

Tip: For practical examples, refer to the pega-model course.

The Page Object model follows an object-oriented design pattern, representing web pages or application screens as classes. Within these classes, various elements on the page are defined as variables. User interactions, such as filling fields and clicking buttons, are implemented as methods within the class.

The "Problem" with Direct Approach

Let's consider an example where we need to log in to the application at each step:

import { Feature, Selector, Controller } from "test-maker";

Feature(`Example Feature`)
.Scenario(`Example Scenario`)
.Given(`Step 1`, async (I) => {
await I.fillField(`#username`, `john@example.com`);
await I.fillField(`#password`, `password`);
await I.click(`#submit`);

/* Rest of the step code */
})
// ... (repeating login steps)

This approach is verbose, and the login steps are repeated multiple times, resulting in a maintenance challenge.

Optimizing with Page Object Model

./src/page-models/login.ts

import { I, Selector } from "test-maker";

export class LoginPageModel {
public userNameSelector = Selector(`#username`);
public passwordSelector = Selector(`#password`);
public submitSelector = Selector(`#submit`);

public async login(username: string, password: string): Promise<void> {
await I.fillField(this.userNameSelector, username);
await I.fillField(this.passwordSelector, password);
await I.click(this.submitSelector);
}
}

export const loginPageModel: LoginPageModel = new LoginPageModel();

Now, let's break down what we've done:

  1. We create a class called LoginPageModel, which will house our methods.

  2. We abstract the selectors into class properties. This is essential for maintainability. If, for instance, the username selector changes, you only need to update it in one place.

  3. We create a login method that accepts username and password as parameters, making it more adaptable.

  4. We create an instance of the LoginPageModel class, which is initialized once during the entire testing process.

Using the Page Object Model

import { Feature, Controller } from "test-maker";
import { loginPageModel } from "../aaa";

Feature(`Example Feature`)
.Scenario(`Example Scenario`)
.Given(`Step 1`, async (I) => {
await loginPageModel.login(`john@example.com`, `password`);

/* Rest of the step code */
})
// ... (using the Page Object Model for login)

By adopting the Page Object Model, you reduce 3 lines of code to just 1. This not only makes your tests more concise but also significantly simplifies maintenance.

Enhancing Readability and Beyond

When placed side by side, the straightforward description of your abstracted function, loginPageModel.login(username, password), stands out for its simplicity. This is particularly evident when compared to the alternative, which involves a series of fillField actions and selector clicks, potentially lacking clarity about the element's purpose.

The Page Object Model not only optimizes code readability but also sets the stage for efficient test management. To take your testing to the next level, consider combining the Page Object Model with a data-driven approach. For more details, check here.

Page Models are essential for improving the readability, maintainability, and efficiency of your test automation. Make sure to implement this best practice in your testing process.