How To Speed Up Cypress Tests With Reusable Sessions?
Test execution is all about speed. Lately we hear all the time about speed of execution of tests and which tool is faster. It has become the most important thing in the world of automated testing. It is all about how fast can we get a feedback about the latest changes. Having slow tests can cost organizations a lot of money. If they cannot get a fast response from test suite about the stability of the system under test it may happen that engineers skip running the tests. There are ways to speed up your tests. Today we will discuss how to reuse cookies within sessions to avoid multiple logins. Let’s answer the question: how to speed up Cypress tests?
The problem definition
A good practice in test automation is to make all your tests independent. If you have tests depending on each other then you may face chained failures and the dependent tests will be skipped. A good practice would be to clear session before each it
block. Cypress lately does that by default unless you set it otherwise. Now, if you allow the test to start over for each it
block that would mean that your test users have to login every time again. This means that we may need to duplicate the code for login. Yes, we can avoid this by using beforeEach
and keep our tests DRY. But there would still be a matter of logging in several times which definitely slows down the execution of the tests. In the following example I have three simple tests which execute in 11 seconds.
describe('Debugging Cypress tests', () => {
beforeEach('Open Demo Blaze website', () => {
cy.loginWithoutReusableSession('qaEssentials', '123456789');
});
it('Should have 9 items on home page', () => {
//check the number of items on the home page
cy.get('.card').should('have.length', 9);
})
it('Should add Nexus 6 to the cart', () => {
//click on Nexus 6
cy.get('a').contains('Nexus 6').click();
//check the response of the server
cy.intercept('POST', 'https://api.demoblaze.com/view').as('view');
//check name of the product
cy.get('.name').should('have.text', 'Nexus 6');
//click on Add to cart
cy.get('.btn-success').click();
//check the alert message
cy.on('window:alert', (str) => {
expect(str).to.equal('Product added');
return true;
});
});
it('Should check visibility of the Place order button', () => {
//click on Cart button
cy.get('.nav-link').contains('Cart').click();
//check name of the product
cy.get('[data-target="#orderModal"]').should('be.visible').and('be.enabled');
});
});
And I have custom command for login like this:
Cypress.Commands.add('loginWithoutReusableSession', (username, password) => {
cy.intercept('GET', 'https://api.demoblaze.com/entries').as('entries');
cy.visit('https://www.demoblaze.com/index.html');
cy.wait('@entries');
cy.get('#login2').click();
cy.get('#loginusername').should('be.enabled').type(username);
cy.get('#loginpassword').should('be.enabled').type(password);
cy.get('button').contains('Log in').click();
});
A better approach
To improve the test speed I will change the custom command in commands.js file. I need to encapsulate the entire login logic with cy.session
command.
Cypress.Commands.add('loginWithReusableSession', (username, password) => {
cy.session([username, password], () => {
cy.intercept('GET', 'https://api.demoblaze.com/entries').as('entries');
cy.visit('https://www.demoblaze.com/index.html');
cy.wait('@entries');
cy.get('#login2').click();
cy.get('#loginusername').should('be.enabled').type(username);
cy.get('#loginpassword').should('be.enabled').type(password);
cy.get('button').contains('Log in').click();
});
});
Now in the test itself I will add this custom command in each it
block along with cy.visit
command to make this session reusable.
describe('Debugging Cypress tests', () => {
// beforeEach('Open Demo Blaze website', () => {
// cy.loginWithReusableSession('qaEssentials', '123456789');
// });
it('Should have 9 items on home page', () => {
cy.loginWithReusableSession('qaEssentials', '123456789');
cy.visit('https://www.demoblaze.com/index.html');
//check the number of items on the home page
cy.get('.card').should('have.length', 9);
})
it('Should add Nexus 6 to the cart', () => {
cy.loginWithReusableSession('qaEssentials', '123456789');
cy.visit('https://www.demoblaze.com/index.html');
//click on Nexus 6
cy.get('a').contains('Nexus 6').click();
//check the response of the server
cy.intercept('POST', 'https://api.demoblaze.com/view').as('view');
//check name of the product
cy.get('.name').should('have.text', 'Nexus 6');
//click on Add to cart
cy.get('.btn-success').click();
//check the alert message
cy.on('window:alert', (str) => {
expect(str).to.equal('Product added');
return true;
});
});
it('Should check visibility of the Place order button', () => {
cy.loginWithReusableSession('qaEssentials', '123456789');
cy.visit('https://www.demoblaze.com/index.html');
//click on Cart button
cy.get('.nav-link').contains('Cart').click();
//check name of the product
cy.get('[data-target="#orderModal"]').should('be.visible').and('be.enabled');
});
});
The result I have achieved with this change:
The entire spec with the same three tests like before was executed in 5 seconds. That is 6 seconds faster than the previous test run.
How do I know it is using the session? Well, first when I look at the execution I don’t see the user is logged in three times, before each test block. Second, I see in the runner when the session was created and when it was restored. Here is an example after I moved the custom command and cy.visit
line in the beforeEach
block to avoid code duplication.
Alternative scenarios
The same can be used if you have different users, as Cypress documentation explains it. In this case you need to place the custom command in the specific it
block where you want the second user to log in, pass the alternative credentials and this works just fine. There is also and option to make the session persistent across different specifications. In this case, you need to add another parameter to cy.session
called cacheAcrossSpecs and assign true
to it. My code would like this with that option.
Cypress.Commands.add('loginWithReusableSession', (username, password) => {
cy.session([username, password], () => {
cy.intercept('GET', 'https://api.demoblaze.com/entries').as('entries');
cy.visit('https://www.demoblaze.com/index.html');
cy.wait('@entries');
cy.get('#login2').click();
cy.get('#loginusername').should('be.enabled').type(username);
cy.get('#loginpassword').should('be.enabled').type(password);
cy.get('button').contains('Log in').click();
}, {cacheAcrossSpecs: true});
});
You can use cy.session
with API login as well. I didn’t have any publicly available application with API login which I can use for this example, so I will copy the code from the official Cypress pages.
cy.session(username, () => {
cy.request({
method: 'POST',
url: '/login',
body: { username, password },
}).then(({ body }) => {
window.localStorage.setItem('authToken', body.token)
})
})
Conclusion
Cypress allows us to reuse the session with cookies created during login for example. Although this is most used example for session reusing I believe that in specific cases this command can be used for restoring all kinds of cookies and tokens. I tried with cookie consent as well and Cypress restored the cookies successfully. At the beginning of the article you asked how to speed up Cypress tests and cy.session is a definitive answer to this question. As you have seen in the above example, a significant time savings can be achieved with this command. If you use the possibility to restore the sessions across the specifications I believe that overall savings can be even more significant.