Page objects vs. App actions in Cypress

Advanced Topics — February 4, 2021

If you started using Cypress in recent years, chances are that you heard about “app actions” being preferred over using page objects. In fact, if you google “Cypress page objects” the first article you’ll see is Gleb Bahmutov’s blog titled:

Stop using Page Objects and Start using App Actions

I see this statement confusing some people, making them think that using Page Object Model (POM) is some sort of anti-pattern in Cypress. The truth is, that using POM does no harm in Cypress and can be really helpful. But I like that Gleb has made a strong argument for other options. Because using them unleashes some Cypress superpowers. In this post, I’d like to explore and describe both of these options.

Page objects

Gil Tayar has made an excellent demonstration of how to use page objects in Cypress. If you are new to this concept or haven’t yet used page objects in Cypress, I suggest you watch that video. In fact, go watch the full course on Test Automation University. It’s free! To sum things up for this article, let me just give you a very quick TL;DR version of how to create page objects.

For demonstration, you can take a look at my Trello clone app. Make sure you check out the page-objects-app-actions branch.

Let’s say we want to work with the login/signup part of our app. Normally this is the part where many of our test efforts start. To create a page object, create a separate .js or .ts file where all your functions for a particular component will be stored.

I will create a login.ts file that will look like this:

I am creating a Login class, which contains two functions. One will open my login window, and the other clicks a link to the signup page. So basically, I’m grouping sequences of Cypress commands into these functions. These help me divide my test code into smaller chunks that I can later reuse.

Paraphrasing Gleb from Cypress again –

It’s all JavaScript, you can do whatever you want.

And he’s right. I can add actions, assertions, or pass arguments into these functions. Let’s see what these two functions do in our application (I slowed the video down a little so it’s clear to see):

In my test, I want to open the login modal window, and then click on the „Sign up here“ text. This is what our test code looks like, using functions from our page object:

Embedding: https://gist.github.com/filiphric/8fb54a00bbd40280cd30b134c897dc44.js?file=login.spec.ts

Notice how our functions are chained together. This is because in each of our page object functions I’m using 

return this;

This way, each of our functions returns to the original context and will be chained off our Login class.

Now, whenever I want to create a flow that includes clicking on a “Log in” button, I can use a function from my page object. Since I have this action abstracted in page object, in theory, I can now write tests for login, signup, reset password, logout, and many other user stories.

Also, whenever something in our login flow would change, I can just edit my page object and that change translates everywhere. This helps avoid situations when I need to rewrite multiple tests because of a single change in the tested app.

Page objects are also a great way to get your app into the desired state that you want to test functionally or with visual tests.

These are some main reasons why testers abstract their UI actions into page objects. So why wouldn’t you use them in Cypress?

Why don’t just stick with page objects?

The main reason for choosing a different approach is to take advantage of how Cypress is built. Cypress runs inside the browser, which is the main difference compared to Selenium-based test automation. That means that Cypress can actually get access to a lot of what’s happening inside of our application.

For example, we can call the exact same function that gets called when we click on the “log in” button. In other words, we can open our login modal window without actually doing the click. That way, we can skip interacting with UI through our page objects, and just start our app in any state we want. This enables us to avoid doing UI actions that are not actually a part of our test.

In our app, let’s say we want to do a visual test for our signup view. In order to create such a test, we need to:

  1. Click “log in” button
  2. Click “sign up here” link
  3. Do our visual test

Notice, that steps 1 and 2 are not actually part of what we want to achieve. We are here just for the state that we want to test visually. Furthermore, I imagine this is an example of a very simple flow. There may be some hard-to-reach places in your app that require you to do multiple steps. So how about we just:

  1. Set our app to the desired state
  2. Do our visual test?

This is what app actions actually do. Instead of clicking, typing, and interacting with our app, we will set it up the way we want.

Before we write our first app action, let’s examine how our app actually works.

Looking into the app

Our app is built with Vue.js. If you are not familiar with it, I suggest you check it out. It provides a great developer experience. For some reason, Vue was the easiest to learn for me as a tester out of the most popular frameworks. But if you are working with Angular or React, principles are pretty much the same.

Vue.js comes with some amazing developer tools. These enable you to change the state of your app right from devtools. Look at how I’m able to toggle the visibility of our login element with these tools:

You can see that I have an attribute called 

showLoginModule

that can be set to true or false. With Cypress, we are going to change the state of our app in a very similar way. To do that, we need to first expose our app to our window context. This may sound like a challenge, but in our Vue app is just done by adding the last line in this piece of code:

Whenever we now open our dev tools and type window.app into our console, we will see our attributes, app functions, and much more information about our app. In fact, if we decide to type 

window.app.showLoginModule = true 

into our console, our login modal window will open. Go ahead and try that!

Creating an app action

Now, we can do the exact same thing using Cypress, like this:

Embedding: https://gist.github.com/filiphric/8fb54a00bbd40280cd30b134c897dc44.js?file=loginAA1.spec.ts

And just like that, our login modal is open! We have made our first app action. In other words, we have set our app to the desired state. Let’s go one step further and get to our signup page. Now, instead of simply setting our attribute to the desired state, we will call a function that changes the state for us.

To save us some time, I have created a helper custom command in our app, that will select one of our components:

  • root
  • Navbar
  • Login
  • board-collection
  • board

These are the same components you can see in Vue.js dev tools. You can use the command simply by calling 

cy.component('Login')

This command will return our component with all its data and functions:

Notice that this component contains a 

logSignSwitch() 

function. If you look into the code, you’ll see that this is a function responsible for switching between “login” and “signup” view in our modal window.

We will now call this function from within our test using the following code:

Embedding: https://gist.github.com/filiphric/8fb54a00bbd40280cd30b134c897dc44.js?file=loginAA2.spec.ts

This will now open our app in desired state, which is our signup screen.

Why bother though?

This is a legitimate question. Seems like the setup is more complicated than setting up page objects. However, one does not rule the other. You can combine both approaches and use the one that is more appropriate for your test. It is however good to design your tests in a way that will shorten them by starting it at a desired point.

App actions can also be quite fast. During my comparison of these two tests, I got to finish my tests in 0.34 seconds for page objects and 0.18 seconds for app actions. Although both are pretty fast, things can really make a difference when running thousands of tests.

App actions also skip the part where e.g. your inputs need to become interactive, our your buttons need to become clickable. There are no incomplete input fields or misclicks because we are skipping this kind of interaction altogether.

Of course, we cannot write an end-to-end test without interacting with our application directly. For these parts, abstracting common actions make absolute sense. But for the parts of our app that are already tested, we can save some time and skip a few steps.

I find app actions especially useful for setting up your app for a visual test. You may save yourself a few headaches by skipping UI interaction and jumping straight into your desired state.

If you liked this blog, be sure to check out my page at filiphric.com, I write a Cypress tip every week and do all kinds of fun stuff 🙂

Are you ready?

Get started Schedule a demo