Challenging the Page Object Model

Sebas Viquez
5 min readFeb 9, 2024

_This is the story of a small but brave warrior that challenged the giant_

Courtesy of Copilot

The Page Object Model (POM) has long been hailed as a best practice for enhancing the maintainability and readability of test scripts. However, as with any established methodology, there are always dissenting voices challenging the status quo.

In this article, we will explore the reasons why some advocates argue against using the Page Object Model and consider alternative approaches for test automation.

One of the primary criticisms of the Page Object Model is the potential overhead in maintenance. While POM encourages the creation of separate classes representing each web page or component, any changes to the UI can lead to corresponding changes in these classes. In dynamic applications, where UI elements frequently evolve, this can result in a continuous cycle of updates to the Page Objects. This constant maintenance can be time-consuming and may offset the initial gains in readability. Ironically, this is one of the reason why the POM exists.

We also have to take into consideration that enforcing a rigid structure that mandates creating a separate class for each page may not necessarily align well with the structure of the application being tested. In scenarios where the application does not neatly fit into the page-centric model, adhering strictly to POM might lead to convoluted and unnatural representations of the application components.

Do not repeat yourself == Do not repeat yourself, just like I did. POM can lead to code duplication when similar UI elements exist across different pages. Maintaining separate Page Objects for similar elements on multiple pages can result in redundant code, making it harder to keep the codebase DRY (Don’t Repeat Yourself).

Now, lets talk about alternative approaches using Cypress.

Cypress provides flexibility that allows teams to explore alternative approaches. Custom commands, utility functions, and a component-based testing approach can address some of the concerns and comments mentioned above.

Component-based testing is an approach that treats UI components as individual entities, focusing on testing the functionality and behavior of each component independently. This approach is particularly useful when dealing with modern web applications built using component-based frameworks like React, Angular, or Vue.js. See https://docs.cypress.io/guides/component-testing/overview

But, in this particular case, let’s elaborate on custom commands and utility functions.

Custom Commands

Custom commands in Cypress allow you to create reusable actions that can be invoked across multiple test cases. These commands help encapsulate complex or frequently used sequences of actions into a single, easy-to-use function.

So let’s create a custom command! In this example, we’ll create a custom command to log in, utilizing Cypress best practices such as handling asynchronous behavior, managing environment variables, and improving error handling.

Create a ‘commands.js’ file in your Cypress support folder (usually ‘cypress/support’).

// cypress/support/commands.js
Cypress.Commands.add('login', (username, password) => {
// Log in and handle asynchronous behavior
cy.request({
method: 'POST',
url: '/login', // Replace with your login endpoint
form: true,
body: {
username,
password,
},
}).then((response) => {
// Check if login was successful based on the response
if (response.status === 200) {
Cypress.env('loggedInUsername', username);
} else {
// Handle login failure gracefully
cy.log(`Login failed for user: ${username}`);
cy.log(`Error message: ${response.body.error}`);
throw new Error('Login failed');
}
});
});

Include the ‘commands.js’ file in your ‘index.js’ file.

// cypress/support/index.js
import './commands';

Now, you can use the ‘cy.login()’ command in your test cases. This custom command encapsulates the login functionality and handles errors gracefully.

// cypress/integration/login_spec.js
describe('Login Tests', () => {
beforeEach(() => {
// Perform any necessary setup, like visiting the login page
// cy.visit('/login');
});


it('should log in successfully', () => {
// Use the custom command to log in
cy.login('testuser', 'password');
// Assert the expected outcome
cy.url().should('include', '/dashboard');
});


it('should display an error for invalid credentials', () => {
// Use the custom command to attempt login with invalid credentials
cy.login('invaliduser', 'wrongpassword');
// Assert the expected outcome
cy.get('.error-message').should('be.visible');
});
});

In the custom command, we use ‘Cypress.env()’ to store the logged-in username in the Cypress environment. This is a recommended practice for managing state across tests. You can access this environment variable in other tests or custom commands.

// Accessing the environment variable in another test
const loggedInUsername = Cypress.env('loggedInUsername');

The custom command includes error handling to gracefully handle login failures. If the login request fails, an error is thrown, and relevant information is logged. This helps in diagnosing and troubleshooting issues during test execution.

Lets explore utility functions now!

Utility Functions

Apart from custom commands, you can create utility functions to encapsulate actions that are not directly tied to Cypress commands but are still reusable across multiple tests.

Example:

Suppose you have a utility function to generate a random username for testing purposes:

Create a ‘utilities.js’ file in your Cypress support folder:

// cypress/support/utilities.js
export const generateRandomUsername = () => {
const timestamp = new Date().getTime();
return `user${timestamp}@example.com`;
};

Include the ‘utilities.js’ file in your ‘index.js’ file:

// cypress/support/index.js
import { generateRandomUsername } from './utilities';
Cypress.env({ generateRandomUsername }); // Make it available globally if needed

Now, in your test cases, you can use the ‘generateRandomUsername’ utility function:

// cypress/integration/register_spec.js
describe('Registration Tests', () => {
it('should register a new user with a random username', () => {
const username = Cypress.env('generateRandomUsername')();
// Perform registration with the generated username
});
});

As shown above, by encapsulating common actions in custom commands or utility functions, you can easily achieve code reusability, enhance readability, and make your Cypress test suite more maintainable. It also allows for easier updates and modifications to shared actions.

And that’s a wrap!

While the Page Object Model has been a go-to recommendation for test automation, Cypress provides flexibility that allows teams to explore alternative approaches. Custom commands, utility functions, and a component-based testing approach can address some of the concerns previously mentioned. Nevertheless, teams should carefully consider their project requirements, team expertise, and testing goals when choosing the most suitable approach.

Final thoughts_

Make time for testing, and use it wisely, as time is not a constraint, it is a gift.

Cypress.io

--

--