Writing Better Tests With Cypress' Page Object Model

Written by bormando | Published 2022/10/10
Tech Story Tags: cypress | automation | test-automation | test | page-object-model | software-testing | unit-testing | software-development

TLDRThere’s no official documentation on **Page Object Model (POM)** implementation in **Cypress**. **POM** is a test automation programming (or design) pattern. POM is still the most popular test automation design pattern and it works pretty well with.Cyppress**. You’ve already searched online with keywords `cypress page object model` and read [the article that says not to use this design pattern] and read it here.via the TL;DR App

Hello everyone!

I was inspired to write this article by the fact that there’s no official documentation on Page Object Model (POM) implementation in Cypress. You might find some articles online, but I find them imperfect - I’ve got a thing or two to add.

I also suggest that you’ve already searched online with keywords cypress page object model and read the article that says not to use this design pattern. Don’t fool yourself that this is the Cypress canon usage just because the article is posted in Cypress blog (at least scroll down to the comments section and you’ll see what I mean). POM is still the most popular test automation design pattern and it works pretty well with Cypress!

WHAT’S POM?

Page Object Model (POM) is a test automation programming (or design) pattern. It’s built on top of the Inheritance paradigm of OOP (object-oriented programming).

As it’s a design pattern and it’s not unique for every web automation tool, no wonder there are no official Cypress docs for that ☝️

Key concepts of POM are:

  1. Some pages have mutual logic

    1. This logic may be shared in a single place to avoid code repetition (parent class to child classes)
  2. Pages have elements and methods

    1. Element = DOM tree element
    2. Method = sequence of actions on the page
  3. Pages are presented as classes, elements as class attributes, and methods as class methods

IMPLEMENTATION

Here’s an example GitHub repo with tests for Swag Labs example web app.

  1. Create a folder for your Page Objects, preferably name it pages and place it in your cypress directory, like this:

  1. Create page.js inside of pages folder - that’ll be a parent page (class) for all other pages. It’ll contain selectors/methods that can be used on EVERY page of the app.

  1. Create Page a class inside of page.js and fill it with some basic stuff. No worries if there’s not much content in the beginning - you’ll fill it up later!
class Page {
  open(path) {
    return cy.visit(path)
  }
}

export default Page

  1. Create auth.page.js right near page.js and fill it with an AuthPage class by extending existing Page class:
import Page from './page'

class AuthPage extends Page {
  get inputUsername() {return cy.get('[data-test="username"]')}
  get inputPassword() {return cy.get('[data-test="password"]')}
  get buttonLogIn() {return cy.get('[data-test="login-button"]')}
  get containerError() {return cy.get('[data-test="error"]')}

  open() {
    return super.open('/')
  }

  logIn(username, password) {
    this.inputUsername.type(username)
    this.inputPassword.type(password)
    this.buttonLogIn.click()
  }
}

export default new AuthPage()

  1. Now you simply import the page that you need in your test (spec or cy) files and call its selectors/methods:
import AuthPage from '../pages/auth.page'
import user from '../fixtures/user.json'
import error from '../fixtures/error.json'

describe('Authentication', () => {
  beforeEach(() => {
    AuthPage.open()
  })

  it('With existing credentials', () => {
    AuthPage.logIn(user.username, user.password)
    cy.location('pathname')
      .should('include', 'inventory')
  })

  it('With non-existing credentials', () => {
    AuthPage.logIn('foo', 'bar')
    AuthPage.containerError
      .should('have.text', error.credentials)
  })
})

And that’s pretty much it ☝️

Now compare it with the raw approach, when you don’t use POM:

import user from '../fixtures/user.json'
import error from '../fixtures/error.json'

describe('Authentication', () => {
  beforeEach(() => {
    cy.visit('/')
  })

  it('With existing credentials', () => {
    cy.get('[data-test="username"]')
      .type(user.username)
    cy.get('[data-test="password"]')
      .type(user.password)
    cy.get('[data-test="login-button"]')
      .click()
    cy.location('pathname')
      .should('include', 'inventory')
  })

  it('With non-existing credentials', () => {
    cy.get('[data-test="username"]')
      .type('foo')
    cy.get('[data-test="password"]')
      .type('bar')
    cy.get('[data-test="login-button"]')
      .click()
    cy.get('[data-test="error"]')
      .should('have.text', error.credentials)
  })
})

I think it’s straightforward that POM lets you get rid of repetitive code blocks and make tests much more readable.

BEST PRACTICES

Method creation for every action

It doesn’t make any sense if you create methods for simple actions - you’re just writing more code and making Page Objects larger, which can’t be good.

❌ DO NOT create methods for simple actions (i.e. click, type text, etc):

LoginPage.clickLogInButton()

USE THIS instead:

LoginPage.buttonLogIn.click()

Huge base page class

You should only add those selectors & methods to the parent page, which are related to ALL child pages.

If there’s a logic that’s not related to any page, consider using Cypress Custom Commands.

If you have multiple pages that share the same elements/methods but you can’t group them under a single parent class - you may create a Page Element for such logic and include it in pages where it’s required (but it’s a topic for another article).


Written by bormando | Head of QA @ OttoFeller, speaker, mentor, contributor
Published by HackerNoon on 2022/10/10