Using PageObject pattern with Cypress

Anton Kravchenko
Geek Culture
Published in
5 min readNov 20, 2021

--

Photo by Christopher Gower on Unsplash

The article showcases how to use the PageObject pattern with Cypress and cypress-selectors.

Page Object Model (POM) is a design pattern that aims to represent a web page as a class describing elements of that page as class fields and behavior of that page as class methods. Such class suites as both repository of web elements on the page and interface for interactions with those elements. This approach helps create an abstraction over the application's layout and encapsulates the mechanics of querying and interacting with elements behind an application-specific API.

Page Object Model pattern has many advantages:

  • it introduces convenient abstraction for interaction with UI
  • it encapsulates the details of the UI structure of a page and its behavior in a single class
  • it enables code re-use since any page object may be used in many test cases
  • it simplifies tests maintenance as it is easy to locate pages and elements that need to be updated
  • it makes tests cases more readable and easier to understand since POM hides the complexity of querying elements behind simple high-level API

POM is a widely adopted pattern, and de-facto is a standard approach for E2E testing. Even though the Selenium web testing framework has coined the pattern's name, the POM pattern can be used with other testing frameworks. In particular, I found the POM pattern helpful in tests written in Cypress, so let's discover how to use these two in conjunction.

As an application under test, I'm going to use one of the implementations of the real-world app called conduit. It is a clone of Medium.com, where you can create an account, post a new article, view the feed, and so on.

Here are the three flows we're going to cover with tests:

  1. Login flow — user logs in to the application
  2. Post new article flow — user logs in to the application and publishes an article
  3. List publications flow — user logs in to the application and opens the list of articles

Let's start with defining our page objects. The test cases cover five different pages, so we need to declare five classes: LoginPage,HomePage, NewArticlePage, ArticlePage and ProfilePage.

Let's start with the LoginPage:

The first class declares selectors as static fields using decorators from the cypress-selectors library. emailInput and passwordInput elements will be selected by the value of the attribute type. signInButton will be pointing to the second element on the page with the text 'Sign in.'

The second class is the page object for the Login page. It implements the only thing that can be done on that page — logging in. The reason for such separation of selectors and page behavior is that it makes it easier to re-use selectors without creating instances of page objects when it is not necessary. Your application might have "global" elements which are always present on the screen and represent the application's state (for example, a header that renders your information if you are logged in or a login button otherwise). In such cases, it might be a good idea to describe controls of such elements just one time and use them everywhere instead of declaring the same selectors in many classes.

Take a closer look at how the selectors from LoginPageSelectors class are being used. It is worth pointing out that even though selectors have been declared statically and are being used by reference, the actual element look-up happens right before the next command. For instance, here LoginPageSelectors.emailInput.type(email) Cypress will search for emailInput element right before executing type method.

There is a bit of trickery happening under the hood every time a selector is being used, but the only thing to know is that selector doesn't hold a reference to any particular DOM element. Instead, it queries an element and returns you a reference to aCypress.Chainable(the same way as cy.getcommand would do) every time you call it. So that the following two lines of code are equivalent:

LoginPageSelectors.emailInput.type(email);
cy.get('[type="email"]').type(email);

The next is HomePage:

The approach here is the same: one class with static declarative selectors and the second one representing all interactions that can be done with the page. In this example, HomePage suites as a 'router,' so we have three methods that take you to different pages and return instances of page objects for those pages.

The next class is NewArticlePage:

This page allows creating a new article with a title, description, content, tags, and publishing it. Please note that most of the methods return this. It allows convenient chaining in the tests when you run a couple of subsequent actions on a single page.

Last but not least, we need to declare selectors for ArticlePage and ProfilePage:

Now we're all set up to start implementing the tests. Let's start with the login flow. The test will open the login page, enter credentials, attempt to sign in to the application, and verify that the login has succeeded.

That's it — nice and simple. A real-world test case would be way heavier, but the idea here is that POM helps hide a lot of implementation details behind a simple interface. So when you've finished declaring selectors for the elements and describing the behavior of your page, you can use it to define concise and readable tests. Below is the same test implemented without page object and selectors:

The test is cluttered with UI details, and it is hard to tell what it actually does. It is easier to comprehend and work with a test when it utilizes high-level domain concepts (like in our case login flow) than when it doesn't use any abstraction and queries elements by hardcoded selectors.

The next flow we're going to test if publish article flow:

This test is a little beefier — we log in, obtain a reference to the new article page object, populate the new article form with the data, publish it and finally assert that the article has been successfully posted. Even though this test is more complicated than the previous one, it remains readable and easy to comprehend.

And finally, the last test covers the profile page:

Here we log in to the application, open the profile page, and verify that the feed of articles contains the recently published article.

As you can see from examples, POM makes life easier in many ways. After investing some time in creating page objects, you get many benefits such as high readability and lower maintenance cost.

It is worth pointing out that the POM pattern is considered an anti-pattern in Cypress. According to the article, page objects are hard to maintain and make tests slower because they force to build the desired app state through UI instead of taking shortcuts (like modifying the app's state using actions). These points are valid in some cases; however, I found the POM helpful, and the benefits of using it outweigh the cost of introducing it to the project.

The repository with the complete example is available on Github.

Thank you for reading the article, and happy coding!

--

--

Anton Kravchenko
Geek Culture

JavaScript developer, a fan of static type-checking, serverless technologies and unit/E2E testing.