With many test automation efforts, dealing with login is the first hurdle to overcome. This can in itself be quite the challenge.
A most common way to solve a login flow is to simply go through this as a normal user would. Let’s see how this will look in our app:
Cypress tries to clear browser data in between tests, which leads to the need of logging in before every rest. We can either do this by using
beforeEach() hook or by using cookies api to ignore deleting certain cookies from our app.
It makes sense to abstract the login sequence into a separate entity. A go to solution is a page object. In many cases it might look something like this:
This way we can easily login before every test and arrange our test before making actions in our test scenario. If you want to do this globally, you can just add a global
beforeEach() hook into your support file:
I personally prefer using custom commands for widely used functions like this. The big advantage of custom commands is that they become part of your Cypress library. Because of this, they are easy to find, and also play well with Cypress’ chaining syntax. You can also create DOM snapshots for debugging and many more. I write about these in more detail in my previous blogpost. A custom command for a login might look like this:
Lines 1 - 15 are adding our custom command to the Cypress library by expanding TypeScript definitions. Lines 17 - 24 is a function definition that contains the login sequence. Finally, we add our function on line 26. Similarly as with our page object example, we can add our newly created
cy.login() command to a global
beforeEach() hook and make our test log in before every
However, using UI is not the most effective way and will definitely take a performance toll. The second option is to log in programatically. But of course, this is often easier said than done. With programmatic login (or any kind of login for that matter), there are 3 parts present:
On log in, server provides data (usually in form of token), and then frontend saves it to browser (usually in form of cookies). To be able to log in programmatically, you need to understand how your app is sending data to the server and how it handles the response. Basically, you need to know exactly what happens when user fills in the information and hits "Log In" button.
In other words, you will be re-creating that login in your test. It’s pretty darn fast once you figure it out, but not easy. Especially if you need to deal with 3rd party login, OAuth flows and other more advanced login methods. There are many useful guides in Cypress docs for this.
But there’s actually no need to figure out login and be effective. With
cy.session() command, you can use your UI just once for your whole test suite.
cy.session() command came out with version 8.2.0. It was one of the releases that did not get as much attention as they should in my opinion. It’s one of the most effective things you can do in terms of logging in. Let me give you a simple example of the usage and show you how it works. We will use the custom command shown above for this.
In our test, we have wrapped our
cy.login() command inside
cy.session() command. This is giving Cypress the information, that whatever runs inside the
cy.session() callback (in other words, between line 3 and 5) should be remembered as a session. When we run this test again, instead of going through the login, our session will be restored. Notice the following image that shows first run (image above) and second run (image below).
As you can see, the second run is going to restore the session, while the first one is creating it. You can also notice how the second test takes just a fragment of the time compared to first test.
The way it works goes something like this:
cy.session() it will make a decision
performLoginSequence does not exist, I will run the code inside
performLoginSequence exists, I will restore the session
What restoring a session essentially means is recovering all of the browser cookies, local storage and session storage that was present in browser after user was logged in. Remember how we talked about programmatic login and three parts of the login flow? Well,
cy.session() takes care of the browser part, which means you don’t need to worry about the frontend and server part.
We can now take the session to the whole project by inserting the
cy.session() sequence into the support file:
cacheAcrossSpecs option on line 5. This will make login just once for the whole test run. Imagine every login sequence takes 2 second to finish. If you had 100 tests that log in, you just reduced more than 3 minutes from your test run!
Let’s say you have multiple users you want to test your UI against. The first argument of
cy.session() command is actually an alias for the session. This means we can create multiple sessions and give them different names. The easiest way to achieve this is to flip the order of how we write our commands. Instead of wrapping out
cy.login() command inside
cy.session(), let’s make
cy.session() a part of our
cy.login() command, like this:
Notice on line 17, we are giving our session the same name as the email we pass into the
cy.login() function. This means that every user we log in with is going to create their own session. In other words, no matter how many logins we do with different users, each of them will get logged in just once.
You may have heard me saying contradicting things on page object model in the past. I critisize page object model for how it’s being used, but also say that it is not an antipattern. While it makes sense to make abstractions, it really matters how you do them. There’s a big push for using page object model in order to create abstractions and DRY code (DRY = don’t repeat yourself). While I’m all for DRY code, I think as testers we should aim for DRY test execution as well.
Page objects are often used to set up a state of your application. Basically, if you want to test "B", you need to get there by doing "A". If that "A" is a login, you might need to perform it in all of your tests.
cy.session() allows you to limit the amount of times you do "A". This does not have to be a login, you can actually do your setup by calling a bunch of API endpoints or perform some other setup actions.
Using custom commands or custom functions makes more sense than by using a UI page object. But even if you decide to do the setup via UI, you can save a lot of time by using
cy.session(). You would have a hard time using
cy.session() with a page object. It’s not impossible, but essentially you might need to put your whole login action into a single function, where you visit, fill and submit login form. That page object would look something like this:
In this case, our page object contains just a single function and it would not make sense to add any more as this would break the session. Because of this, it makes more sense to choose a simpler pattern, like custom command or a standalone function module that you import to your test.
From time to time I send some useful tips to your inbox and let you know about upcoming events. Sign up if you want to stay in loop.