How to FP

Bottom-upTop-downCypress

Home»Cypress

No more Page Objects

The Page Object design pattern offers two benefits:

  1. It keeps all page element selectors in one place; thus, it provides separation of test code from the locators of the application under test (AUT).
  2. It standardises how tests interact with the page; thus, it avoids duplication of code and eases code maintenance.

Object-orientation (OO) in JavaScript is a bit awkward. Introduction of the class keyword in JavaScript 2015 helped, but classes, and specifically the this keyword, still surprise Java programmers because they work very differently.

Here is a great blog post from Kent C. Dodds which highlights this point.

Enter Page Modules

In Java land, it's pretty common to find Page Objects which inherit from the Base Page. In JavaScript, that might look like this:

1
2
3
4
5
6
7
8
9
10
11
import { HomePage } from './BasePage'

class HomePage extends BasePage  {
  constructor() {
    super();
    this.mainElement = 'body > .banner';
  }
  //... More code

  export const mainPage = new MainPage();
}

With the move to functional programming, we are going to lose not only inheritance, but the class itself. Therefore, we need to use modules to arrange our code. Each module exports public functions that can be imported into other modules and used:

1
2
3
4
5
6
7
8
9
10
11
12
// In the HomePage module (HomePage.js or HomePage/index.js)
export function login (email, password) {
  // code here...
}

export function logout () {
  // code here...
}

export function search (criterion) {
  // code here...
}

This module can then be imported into your tests or other modules and used as below:

1
2
3
4
5
6
7
8
9
// In the HomePageTest module (HomePageTest.js or HomePageTest/index.js)
import * as homePage from './HomePage.js'

describe('Home Page', () => {
  it('User can login', () => {
      cy.visit('/')
      homePage.login('prateek', '123456')
  })
})

or we can import individual functions from a module selectively:

1
2
3
4
5
6
7
8
import { login } from './HomePage.js'

describe('Home Page', () => {
  it('User can login', () => {
    cy.visit('/')
    login('prateek', '123456')
  })
})

In general, import only the functions you need when possible.

But what about inheritance?

public class HomePage extends BasePage { ... } // Java

Often we come across test suites where Page Objects extend a BasePage or every test file extends a BaseTest class.

The intention behind this is typically code reuse. Most often the BaseTest class has methods related to login, logout, logging, etc.

Please don't do that. Bundling unrelated functionality into a parent class for the purpose of reuse is an abuse of inheritance.

Common functionality required by specs can be added as Cypress custom commands. Custom commands are available to be used globally with the cy. prefix. For example, we can add a method called login using a custom command:

1
2
3
4
Cypress.Commands.add('login', (username, password) => {
    cy.get('#username').type(username)
    // code here...
})

The Cypress.Commands.add function takes the name of the custom command as the first argument, and a function as the second argument.

Now we can use that name to call the custom command in any spec:

1
2
3
4
5
6
describe('Login Page', () => {
  it('User can login', () => {
    cy.login('prateek', '123456') // NOT a good password :-)
    // code here...
  })
})

Note: functionality that is shared between a few specs but not all should be added to utility modules instead of adding a custom command. Reserve custom commands for widely-used functionality.

“Favour composition over inheritance”

Why? Watch this video to find out:

Now, consider the code below that uses inheritance (don't do this):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// DON'T DO THIS!
class Person {
  constructor(nameGiven, nameFamily) {
    this.nameGiven = nameGiven
    this.nameFamily = nameFamily
  }

  get fullName () {
    return `${this.nameGiven} ${this.nameFamily}`
  }
}

class Employee extends Person {
  constructor(nameGiven, nameFamily, id) {
    super(nameGiven, nameFamily)
    
    this.id = id
  }
  
  toString () {
    return `My name is ${this.fullName} and my employee ID is ${this.id}.`
  }
}

const employee = new Employee('Lucy', 'Fur', 666)

// Note that `fullName` works even though it is in Person, not Employee
console.log(`employee.fullName: ${employee.fullName}`)
console.log(`employee.toString(): ${employee.toString()}`)
console.log('employee:', employee)

Inheritance is actually a form of object composition. It is the form that binds the objects most tightly, hence it is the one to be most avoided, if possible.

Here we are using inheritance to provide for code reuse. There is no separate Person object to which Employee refers. Employee simply inherits the properties (and methods, if any) of the Person object, so, for example, the fullName getter is available both within Employee (note the use of it in the toString method) and on instances of the Employee class.

The same functionality could be achieved using composition. There are three common ways to compose objects in JavaScript: aggregation, delegation, and concatenation.

Aggregation preserves the objects that we're composing (i.e., aggregating). Here, an Employee is simply a wrapper around a Person that adds an employee id property. A key feature of aggregation is that the Person object retains its integrity and we can extract it from the Employee object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Person factory
const createPerson = (nameGiven, nameFamily) => ({ nameGiven, nameFamily })

// Employee factory (by aggregation)
const createEmployeeByAggregation = (id, person) => ({
  id,
  person
})

// New Person: Jane
const jane = createPerson('Jane', 'Dobbs')

// Make Jane employee #1
const employee = createEmployeeByAggregation(1, jane)

console.log('employee:', employee)
console.log(`employee.id: ${employee.id}`)

// To access the properties of the Person we have to reach into employee.person:
console.log(`employee.person.nameGiven: ${employee.person.nameGiven}`)
console.log(`employee.person.nameFamily: ${employee.person.nameFamily}`)
console.log(`employee.person.nickname: ${employee.person.nickname}`)

console.log(`Set jane.nickname to 'Mom'`)
jane.nickname = 'Mom'

// We can see the new nickname property because our Employee
// simply points to the Person object
console.log(`employee.person.nickname: ${employee.person.nickname}`)

// We can also extract the Person object from the Employee
console.log('Here is our employee.person:', employee.person)

Delegation is similar, except we do not inject the Person into the Employee, so the Person is not directly visible in the Employee and we can't extract the Person back out again as easily.

However, the properties of the Person are directly accessible from within the Employee, and if we mutate the Person (which we won't, right?), then those changes are immediately visible in the Employee that delegates to that Person object.

To pull the Person back out, we've got to get it from the prototype: Object.getPrototypeOf(employee).

Delegation can be used to save memory, while making the properties of Person directly accessible from within Employee (i.e., we don't have to go through the person property):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Person factory
const createPerson = (nameGiven, nameFamily) => ({ nameGiven, nameFamily })

// Employee factory (by delegation)
const createEmployeeByDelegation = (id, person) => Object.assign(
  Object.create(person),
  { id }
)

// New Person: J.R.
const jr = createPerson('J.R.', 'Dobbs')

// Make J.R. employee #2
const employee = createEmployeeByDelegation(2, jr)

// Person properties remain in the Person (jr) and
// are not visible in the Employee object
console.log('employee:', employee)
console.log(`employee.id: ${employee.id}`)

// But we can still reference the Person properties
// as if they were part of the Employee object
console.log(`employee.nameGiven: ${employee.nameGiven}`)
console.log(`employee.nameFamily: ${employee.nameFamily}`)
console.log(`employee.nickname: ${employee.nickname}`)

// And when we update the Person, the Employee properties reflect this
console.log(`Set jr.nickname to 'Bob'`)
jr.nickname = 'Bob'

console.log(`employee.nickname: ${employee.nickname}`)

// Where is the person? In the prototype:
console.log('Object.getPrototypeOf(employee):', Object.getPrototypeOf(employee))
console.log(`Same person? ${Object.is(Object.getPrototypeOf(employee), jr)}`)

Concatenation is the simplest and often the best for our purposes as we always make copies rather than mutating in place. If we are never going to mutate the Person object once we've created it, then why bother linking to it? This is the loosest form of coupling, and should be the first one to which we turn.

Concatenation simply creates a new object, copying the properties from all objects passed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Person factory
const createPerson = (nameGiven, nameFamily) => ({ nameGiven, nameFamily })

// Employee factory (by concatenation)
const createEmployeeByConcatenation = (id, person) => ({
  id,
  ...person
})

// New Person: Marsh
const marsh = createPerson('Marsh', 'Dobbs')

// Make Marsh employee #3
const employee = createEmployeeByConcatenation(3, marsh)

// We've copied all the properties into a new object so all are visible
console.log('employee:', employee)
console.log(`employee.id: ${employee.id}`)
console.log(`employee.nameGiven: ${employee.nameGiven}`)
console.log(`employee.nameFamily: ${employee.nameFamily}`)
console.log(`employee.nickname: ${employee.nickname}`)

// However, we are no longer connected to our Person (marsh)
// so when we update `marsh`, we do not update `employee`
console.log(`Set marsh.nickname to 'Connie'`)
marsh.nickname = 'Connie'

// That said, if we are maintaining immutability, then we will
// never update the Person, so this won't matter
console.log(`employee.nickname: ${employee.nickname}`, '')

Note: Functions which return objects are called factory functions.

In our inheritance example above, Person provided a fullName gettera computed property. The functional way to do this is with a utility function. We could put all our functions used for Person in a Person module rather than a Person class. Then we import that moduleor just the functions we needand use them.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Person factory
const createPerson = (nameGiven, nameFamily) => ({ nameGiven, nameFamily })

// Employee factory (by concatenation)
const createEmployee = (id, person) => ({
  id,
  ...person
})

// Our fullName utility function is effectively polymorphic
// It will work with *any* object that has nameGiven and nameFamily properties
const fullName = ({ nameGiven, nameFamily }) => (`${nameGiven} ${nameFamily}`)

// New Person: Marsh
const marsh = createPerson('Marsh', 'Dobbs')

// Make Marsh employee #3
const employee = createEmployee(3, marsh)

// We've copied all the properties into a new object so all are visible
console.log('marsh:', marsh)
console.log(`fullName(marsh): ${fullName(marsh)}`)
console.log('employee:', employee)
console.log(`employee.id: ${employee.id}`)
console.log(`fullName(employee): ${fullName(employee)}`)

Readability

The reason many people give for using Page Objects is that they encapsulate the complexity of the UI and the locators, which helps with reusability and making the tests more readable. But what is the tradeoff? (There is always a tradeoff.) And is there a better way?

Note: For these examples we are using the Cypress TodoMVC Example Repo and refactoring a few tests.

Consider this typical Cypress example:

1
2
3
4
5
6
7
8
9
10
11
12
describe('Todo Application', () => {
  it('Can add a new Todo', () => {
    cy.get('.new-todo')    // What am I getting here? It's not clear
      .type('First Todo')  // Entering text, so maybe some type of input?
      .type('{enter}')     // Hitting enter, so maybe submitting a form?

    cy.get('.todo-list li')            // Some list element
      .eq(0)                           // Not even sure what this is
      .find('label')                    // Has a label element
      .should('contain', 'First Todo') // That should contain this text. Who cares?
  })
})

As you can see, while the action we're undertakingget, type, eq, find, shouldis obvious, what we're actually trying to accomplish is not clear at all..

Contrast that with this equivalent example:

1
2
3
4
5
6
7
8
9
10
11
12
import { addTodo, getTodoLabel, getTopTodoFromList } from './TodoUtil'

const TODO_LABEL = 'My Todo'

describe('Todo Application', () => {
  it('Can add a new Todo', () =>
    addTodo(TODO_LABEL)
      .then(getTopTodoFromList)
      .then(getTodoLabel)
      .should('equal', TODO_LABEL)
  )
})

The first example is imperative. It tells Cypress how to execute the test. Our second, better example is declarative. We have abstracted away the how and we simply concern ourselves with what to do.

Our declarative second example is far more readable, which means much lower cognitive load for the reader: we can understand it at a glance. Even non-technical personnel, such as your business analyst or product owner, can read and understand this test easily. Note also that we no longer need comments to explain what we're doing.

And we get the added benefit of the reusability of the utility functions addTodo, getTopTodoFromList, and getTodoLabel.

So where can we find our addTodo, getTopTodoFromList, and getTodoLabel functions? They're nicely tucked away in our TodoUtil.js or TodoUtil/index.js file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TodoUtil.js or TodoUtil/index.js
export const addTodo = name =>
  cy
    .get('.new-todo')
    .type(`${name}{enter}`)

export const getTopTodoFromList = () =>
  cy
    .get('.todo-list')
    .eq(0)

export const getTodoLabel = todo =>
  todo
    .find('label')
    .text()

This is where we hide the imperative code. At some low level, we will always need to tell the computer how to do things. But if we abstract this code into library or utility functions with concise, clear names, then our actual code is as easy to read as plain English. That is a major benefit of working in a declarative style.

Now let's look at an update scenario:

1
2
3
4
5
6
7
8
9
10
11
12
13
import {addTodo, getTodoLabel, updateTodo} from './TodoUtil'

describe('Todo Application', ()=> {
  const INITIAL_TODO = 'Initial todo'
  const UPDATED_TODO = 'Updated todo'

  it('Can update a newly created todo', () =>
    addTodo(INITIAL_TODO)
      .then(updateTodo(UPDATED_TODO)) // updateTodo(UPDATED_TODO) is partially applied
      .then(getTodoLabel)
      .should('equal', UPDATED_TODO)
  )
})

And here is the updateTodo utility function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// In TodoUtils.js or TodoUtils/index.js
// Calling this with the updatedLabel returns a FUNCTION
// that takes the todo and updates it
export const updateTodo = updatedLabel =>
  $todo => { // dollar sign indicates this is an element
    cy
      .wrap($todo) // wrap the element to give it Cypress superpowers
      .within(     // scopes all subsequent commands to the $todo element
        () => {    // callback function
          cy
            .get('label')     // grabs the label
            .dblclick()       // double-clicks to open the editor

          cy
            .get('.edit')                   // grabs the editor input
            .clear()                        // clears it
            .type(`${updatedLabel}{enter}`) // enters updated label and submits
        }
      )

    return cy.wrap($todo) // return the wrapped todo element so we can chain `thens`
  }

See how we've hidden the nasty imperative code in our utilities? That allows us to keep our actual test code clean, simple, and easy to understand.

I need more convincing...

OK. Here are three strong arguments against Page Objects:

  1. Page Objects introduce state in addition to the application state, which makes tests harder to understand.
  2. Using Page Objects means that all our tests go through the application's GUI.
  3. Page Objects try to fit multiple cases into a uniform interface, falling back to conditional logicand that's just not nice.

Most of us who swear by Page Objects are coming from Java and Selenium, so it's worth noting that...

JavaScript is NOT Java.

Java is a statically-typed, compiled, strongly object-oriented language (although it is becoming more functional with each new version). JavaScript is a dynamically-typed, interpreted, hybrid OOP/FP language that also gets more functional with each new version (and versions come annually).

New language new paradigm new way of doing things.

But even in an OO paradigm, Page Objects leave a lot to be desired. For example:

Page Objects break the Single Responsibility Principle

Page Objects bind unrelated functionality together in one class. For example, in the code below, searchProduct() functionality is not related to the login or logout actions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// This is JAVA, not JavaScript
public class HomePage {
    private final WebDriver webDriver;

    public HomePage(WebDriver webDriver) {
        this.webDriver = webDriver;
    }

    public SignUpPage signUp() {
        webDriver.findElement(By.linkText("Sign up")).click();
        return new SignUpPage(webDriver);
    }

    public void logOut() {
        webDriver.findElement(By.linkText("Log out")).click();
    }

    public LoginPage logIn() {
        webDriver.findElement(By.linkText("Log in")).click();
        return new LoginPage(webDriver);
    }

    public ProductPage searchProduct(String product){
        webDriver.findElement(By.linkText(product)).click();
        return new ProductPage(webDriver);
    }
}

One major problem with the above code is that our HomePage class does not follow the Single Responsibility Principle (SRP):

The Single Responsibility Principle is a computer programming principle that states that every module, class, or function should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility. [Wikipedia]

The Single Responsibility Principle is part of the SOLID design principles for Object-Oriented Programming. Violating the SRP means that Page Objects aren't even good OOP design practice!

Breaking Page Objects like those above into multiple smaller Page Objects does not pass the SRP smell test, either. For example, we might move the login action outside the HomePage and create a new LoginPage object and use it thus:

1
2
3
// This is Java, not Javascript
LoginPage loginPage = new HomePage().navigateToLoginPage();
loginPage.login("username", "password");

Because these actions belong to two different pages, this code will repeat in every test case that uses login. The responsibility for log in has not been entirely encapsulated.

We can correct this by defining a utility funjction that expresses the intent rather than focusing on the page:

1
2
3
export const loginAsCustomer = (name, password) => {
  /* login code here */
}

Our loginAsCustomer utility function can then work through both the Home and Login screens of the application to complete login as a single user action.

Note: Base your modules on user intent, not on pages.

Page order is not user flow

Another situation in which Page Objects complicate things is when user flows are not the same as the page order.

Consider the example of a shopping website. Here the user can add an item to the cart using either the Product Page or the search functionality on the Search page.

From the Cart page the user may be redirected either to the Home page or to the Search page (e.g., by clicking “continue to shop”), depending on whether the last item was added using the Product Page or the Search Page, respectively.

With Page Objects, the code for the CartPage class might look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
// This is Java, not JavaScript
public class CartPage {       
  Page continueShopping () {
    if (state) {
      // determines using which page the last item was added
      return new SearchPage();
    }
    else {
      return new HomePage();
    } 
  }   
}

Not only is this code more complex to understand, but also it makes it harder to modify the CartPage if in future another user flow is introduced.

This violates the Open/Closed principle (OCP), which is the second of the OOP SOLID design principles, making this doubly bad OOP. And we have to maintain additional state.

The Open/Closed Principle (OCP) states that “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”. [Wikipedia]

We can remove the state logic from our CartPage is by turning the continueShopping into a utility function that simply clicks on the “continue shopping” link:

1
2
3
4
// In CartUtils.js
export const continueShopping () {
  cy.get('#continue').click()
}

Then we can use it in our tests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// In Test.js
import { continueShopping } from './CartUtils'

it('user can add item to cart from home page post selecting "continue to shop"', () {
    /* code to add product to the cart from Product Page */
    continueShopping()
    homePage.navigateToProductPage()
    productPage.addItemToCart('item2')
})

it('user can add item to cart from search page post selecting "continue to shop"', () {
    /* code to add product to the cart using Search */
    continueShopping()
    searchPage.addItemToCart('item')
})

In the above example, our test creates user flows by calling loosely coupled steps in the right order. This means that our individual modules do not have to maintain state.

Loosely-coupled steps

Need another example of how loosely coupled steps reduce complexity? Consider the typical LoginPage class below:

(The business requirement is that on successful login the user is redirected to the Home Page and on unsuccessful login, the user stays on the Login page.)

1
2
3
4
5
6
// This is Java, not JavaScript
class LoginPage {
    HomePage validLogin(String userName, String password) { /* ... */ }
    LoginPage invalidLogin(String userName, String password) { /*... */ }
  }
}

Now, let's introduce roles into the mix. If the user has an Admin role, then they are redirected to the Admin Dashboard on log in. If not, they are redirected to the Home Page. Now we need yet another method in the LoginPage Page Object to return an instance of the Admin Dashboard page:

1
2
3
4
5
6
7
// This is Java, not JavaScript
class LoginPage {
    HomePage validLogin(String userName, String password) { /* ... */ }
    LoginPage invalidLogin(String userName, String password) { /*... */ }
    AdminDashboardPage adminValidLogin(String userName, String password) { /* ... */}
  }
}

More roles will mean even more methods because there is a tight coupling between the pages and the return type. Where will it end?

We can fix this by not returning references to different pages from the login action:

1
2
3
4
5
6
7
const login = (username, password) => {
  cy.get('.username').type(username)
  cy.get('.password').type(password)
  cy.click('.loginButton')
}

export default login

Now our test might look like this:

1
2
3
4
5
6
7
8
9
10
// In Test.js
it('User is taken to Home Page on valid login', () => {
   login('prateek', '12345')
   cy.title().should('equal', 'Home Page')
})

it('Admin is taken to Admin Dashboard on valid login', () => {
   login('admin', '12345')
   cy.title().should('equal', 'Admin Dashboard')
})

Loosely-coupled steps makes for simpler code and fewer lines of code, and that's a big win for everyone.

Resources

Sources

Errors, bugs, suggestions, questions? Contact Prateek Sharma or Charles Munat.