Introduction
Page Objects is a pattern in test automation that allow the automation engineer to encapsulate the data and methods used to support automation of a page. Typically each page of the application will have an automation class that contains data, methods, and locators needed for automation of that page.
In this post I will take a look at a sample Cypress script that does not use Page Objects, and walk you through the steps of converting to the Page Object pattern.
Advantages
So why do we want to use Page Objects? Some advantages of this pattern include:
- separating the implementation complexity of automation from the more business-focused details of the test script steps
- providing a single location for automation details, so if something like a page locator changes in the application, we only need to change it in one place in the automation code
But enough talking, let's get started. Hopefully we can see these advantage in action!
Setup
To make this easy to follow along, I'm going to be starting with the sample test scripts that are included when you install Cypress. Let's do that:
- make sure you have node installed
- create a new folder for our example and from the command line run:
npm init -y
- install cypress with:
npm install cypress --save-dev
- start the test runner with:
npx cypress open
After starting the test runner, you'll see a cypress
folder structure like this:
In the 1-getting-started
folder you'll see todo.spec.js
. This is a sample test that comes with Cypress and demonstrates some basic functionality using https://example.cypress.io/todo as the application under test.
To complete the setup, let's just make sure this test runs properly. Run the test from the command line with npx cypress run --spec cypress/integration/1-getting-started/todo.spec.js
. If things go as planned, you should see messages that 6 tests ran and something like this towards the end:
There we go, setup complete!
Planning our page class - round 1
Let's take a closer look at the todo.spec.js
file. The beforeEach()
hook is not all that interesting, but let's start with that since it's pretty easy to understand.
beforeEach(() => {
cy.visit('https://example.cypress.io/todo')
})
Instead of having the cy
command and the destination URL in the spec, let's create a page object method to deal with all that.
Creating the page object class
The class we're going to create is just a JavaScript class. Create a new folder in the integration folder and name it something like page-objects
. Create a new file names todo-page.js
in the page-objects
folder.
Add the following to the new class file:
export class TodoPage {
navigateToHome() {
cy
.visit('https://example.cypress.io/todo')
}
}
Update the spec to use the page object
We have a new method named navigateToHome()
that we're going to call from our spec file. To make that happen, we just need to make a few updates to the todo.spec.js
file.
- add the following import:
import { TodoPage } from "../page-objects/todo-page"
- instantiate the ToDoPage object prior to the beforeEach hook:
const todoPage = new TodoPage()
- update the beforeEach hook to call the page object method:
beforeEach(() => {
todoPage.navigateToHome();
})
Those are the changes. Run the test again and make sure it's still green:
npx cypress run --spec cypress/integration/1-getting-started/todo.spec.js
And that's about it! We've created a page object class, and used it from our test spec.
Planning our page class - round 2
There are other examples in the spec that might benefit more from Page Objects. Take a look at the first line of the first test:
cy.get('.todo-list li').should('have.length', 2)
This step is verifying that our app has two to-do items. The Cypress interaction detail is right there in the spec, with a lot of locator and cypress detail that could be moved to the page object.
Also what about other tests that follow this pattern? The locator for the to-do list items will be scattered throughout our tests. What if that locator changes in the future? Yup, lots of updates and all the work and risk that comes with that.
I want to move all that detail into one place, the page object. Our spec will become more clear, with a statement such as:
todoPage.validateTodoCount(2)
See how clear that is? It's so clear I am not even going to explain it. After our first example, it's probably pretty clear how to do this in the page object class. Let's take a look.
Updating the page object class
Since we already had the implementation details in the spec, we can just copy/paste/tweak that for our new Page Object method:
validateTodoCount(expectedLength) {
cy
.get('.todo-list li')
.should('have.length', expectedLength)
}
So with those changes to the spec and page object class, run the test again and make sure you are still seeing green tests.
Cool, right? And the great thing? Any test that needs to validate the to-do count in the future can just use this method without worrying about locators or Cypress.
Wrap-up
So with those basic examples I hope you can see the value of encapsulating your automation code into Page Objects. Feel free to experiment on your own and convert ALL the tests in that that spec to use Page Objects. It's a great exercise.
I should also point out that since our Page Object class does not have any instance data, we could have just used functions instead of the class. To learn about this option and much more, I highly recommend you look at Cypress courses Introduction to Cypress by Gil Tayer and Advanced Cypress by Filip Hric, available for free from Test Automation University. Much of my initial Cypress interest and learning came from these 2 courses.
And finally, I would be remiss if I did not mention the article by the Gleb Bahmutov Stop using Page Objects and Start using App Actions where he explores some alternatives to Page Objects.
Feel free to subscribe to my blog site for more test automation content. Thanks!
Top comments (1)
I am not sure if we should wrap every single expectation into the page object function call.
For example in the todo list count example you mentioned, the check can be broken into two parts Locator and Assertion
To me the Locator is the one exposing technical implementation(the CSS selector
.todo-list li
) and it's nice to have a page object likefindTodoItems()
to return that, disregarding the implementation detail and its fragile nature.The second part
Assertion
is, by looking at its raw cypress commandsshould('have.length', 2)
, pretty straightforward. Adding another page object layer likevalidateTodoCount(expectedLength)
seems to be an overkill.As such, I feel we don't always need to include straightforward assertions into page object. Instead, in some assertion that is comprised of couple checks/steps/hacky things, page object assertion-function calls may be easier to understand and let us focus on the higher level business outcome.