10 Best Practices to Improve Your Cypress Testing

cypress best practices

In the current software testing landscape, testing complexity is on the rise. However, modern frameworks like Cypress simplify executing component and end to end testing. Here are some essential considerations for developing an e2e UI testing framework with Cypress.

Defining AUT scopes:

  • The purpose of Cypress testing is purely for automating end-to-end (e2e) tests. So when writing the tests, we should ensure that they focus solely on the application’s functionality/journey.
  • If any third party site, systems dependency is required for the respective test means then we should not have that test in our e2e suite.

If the test involves some other origin, then we need to use the ‘cy.origin()’ method.

Refer: https://docs.cypress.io/api/commands/origin
Better avoid automating anything in the secondary origin URLs.

it('Dont automate anything in the secondary origin', () => {
       cy.visit('https://webdriveruniversity.com/'); //primary origin
       cy.get('#automation-test-store').click();//click the link, which navigates to
       cy.visit('https://automationteststore.com/') //secondary origin in new tab
   });

Test Isolation:

  • The test ‘it’ blocks in the e2e suite should be designed in a way that each blocks are independent and are not linked or dependent on other ‘it’ blocks.
  • This enables the suite to be more robust and we can achieve parallel execution when implemented in continuous integration servers.
beforeEach('Login', () => {
 cy.visit('https://katalon-demo-cura.herokuapp.com');
 cy.get('#btn-make-appointment').click();
 cy.get('#txt-username').type("John Doe");
 cy.get('#txt-password').type('ThisIsNotAPassword');
 cy.get('#btn-login').click();
});

it('using type, checkbox, radiobox in the forms', () => {
 cy.get('#combo_facility').select('Seoul CURA Healthcare Center');
 cy.get('#combo_facility').select(1);
cy.get('#chk_hospotal_readmission').not('[disabled]').check().should('be.checked');
cy.get('#chk_hospotal_readmission').not('[disabled]').uncheck().should('not.be.checked');
});

//This is not the correct way
it(‘same flow- but separate test’, () => { 
 cy.get('.radio-inline [type="radio"]').check('Medicare').should('be.checked');
 cy.get('#radio_program_medicaid').check();
 CalendarUtils.selectDate('.input-group-addon', '24-08-2023');
 cy.get('#btn-book-appointment').click();
 cy.contains('Appointment Confirmation');
 cy.get('div[class="col-xs-12 text-center"] h2', { timeout: 10000 })
 });

Proper usage of custom commands:

  • By default, cypress has the feature of creating custom functions.
  • We can create the function by defining the parameters
  • In the test, we can just invoke the custom function with the expected arguments
  • This will help encapsulate the actions and other logic in the test
Cypress.Commands.add('login', (username, password) => {
   cy.visit('https://katalon-demo-cura.herokuapp.com')
   cy.get('#btn-make-appointment').click()
   cy.get('#txt-username').type(username)
   cy.get('#txt-password').type(password)
   cy.get('#btn-login').click()
})

//Invoking the ‘login’ function in beforeEach

describe('Command specs', () => {
beforeEach('Login', () => {
   cy.login('John Doe','ThisIsNotAPassword')
});


it('add registration details', () => {
   cy.makeAppointment('2023-08-23', 'Seoul CURA Healthcare Center', 'Medicaid');
});
})

By creating this kind of custom commands, we can make the actual tests clean and focus solely on the functionality


Using appropriate function/property names:

  • When using POM [Page Object Model] kind frameworks, we solely create classes, functions, objects, variables, selectors for modularising the framework.
  • For easy maintenance and clean code we should ensure that the naming convention is ‘consistent and relevant’

Reference for Locator naming:

class AppointmentPageObjects {
   // Element references
   buttonMakeAppointment = '#btn-make-appointment';
   textUsername = '#txt-username';
   textPassword = '#txt-password';
   buttonLogin = '#btn-login';
   comboFacility = '#combo_facility';
   checkboxHospitalReadmission = '#chk_hospotal_readmission';
   radioMedicare = '.radio-inline [type="radio"]';
   radioMedicaid = '#radio_program_medicaid';
   textComment = 'txt_comment'
   datePicker = '.input-group-addon';
   buttonBookAppointment = '#btn-book-appointment';
   confirmationMessage = 'div[class="col-xs-12 text-center"] h2';
}

export default AppointmentPageObjects;

Reference for the function naming:

enterUsername(username) {
       cy.get(this.pageObjects.txtUsername).type(username);
   }
enterPassword(password) {
       cy.get(this.pageObjects.txtPassword).type(password);
   }
clickLogin() {
       cy.get(this.pageObjects.btnLogin).click();
   }
selectFacilityByName(name) {
       cy.get(this.pageObjects.comboFacility).select(name);
   }
checkReadmission() {
    cy.get(this.pageObjects.chkHospitalReadmission).not('[disabled]').check();
   }
uncheckReadmission() {   cy.get(this.pageObjects.chkHospitalReadmission).not('[disabled]').uncheck();
   }

Using fixtures for effective data driven tests:

  • By default, cypress has the ‘fixture’ functions, so that we can pass the test datas
  • The most common data type is ‘json’, cypress supports other formats as well, please refer to the documentation https://docs.cypress.io/api/commands/fixture

users.json

[
   {
        "username": "standard_user",
        "password": "secret_sauce"
    },
    {
        "username": "problem_user",
        "password": "secret_sauce"
    },
    {
        "username": "locked_out_user",
        "password": "secret_sauce"
    },
    {
        "username": "performance_glitch_user",
        "password": "secret_sauce"
    }
]

Invoking the test data in the actual tests

it('Test2- login with multiple credentials', () => {
   cy.fixture('users').then((users) => {
       users.forEach(user => {
           // your test logic
           cy.visit('https://www.saucedemo.com/');
           cy.get('#user-name').type(user.username);
           cy.get('#password').type(user.password);
           cy.get('#login-button').click();
          
       });
   });
});

Using web first assertions & having multiple assertions:

  • First, let’s see what is a web first assertion and what is not a web first assertion.
    • Web First Assertions focus on interactions with the web application through its UI or API, simulating user actions and verifying outcomes such as UI updates, navigation, and API responses.
    • Non-Web First Assertions focus on JavaScript functions or logic within the application context and asserting their outcomes. This approach is more about verifying the internal logic than user interaction scenarios.
// Web-first assertion: Wait for a specific elements
cy.get('.success-message').should('be.visible');
cy.get('.radio-inline [type="radio"]').check('Medicare').should('be.checked')
cy.get('.left-nav').children().should('have.length', 8)
cy.get('@xyz').click().should('be.disabled')


//Non-web first assertion:
cy.window().then((win) => {
   const sum = win.add(5, 3);
   expect(sum).to.equal(8);
 });

Designing tests to have multiple assertions in a single test is good, instead of having one specific assertion in separate tests.

//Having multiple assertions in the same test
it('validates and formats first name', () => {
   cy.get('[data-testid="first-name"]')
     .type('johnny')
     .should('have.attr', 'data-validation', 'required')
     .and('have.class', 'active')
     .and('have.value', 'Johnny')
 })


//Having multiple tests with having only one specific assertion
it('has active class', () => {
   cy.get('[data-testid="first-name"]').should('have.class', 'active')
 })


 it('has formatted first name', () => {
   cy.get('[data-testid="first-name"]')
         .should('have.value', 'Johnny')
 })

Using proper waits aka retry-ability:

  • Cypress inherently retries commands that query the DOM.
  • It automatically pauses and reattempts most commands, waiting for the targeted element to appear in the DOM.
  • If an element remains unactionable beyond the time specified in the defaultCommandTimeout setting, the command will be deemed a failure.
//not required
   cy.visit('https://katalon-demo-cura.herokuapp.com')
   cy.wait(5000)
   cy.get('#btn-make-appointment').click()

//instead we can use like this
   cy.visit('https://katalon-demo-cura.herokuapp.com')
   cy.get('#btn-make-appointment').should('be.visible')
   cy.get('#btn-make-appointment').click()

Note: Since we are having the ‘defautCommandTimeout’ in our config, cypress will retry till the configured timing for command and assertion.

So if ‘#btn-make-appointment’ is not available instantly, cypress will retry the assertion until the configured timing, and eventually this will sort out the usage of ‘cy.wait()’ there.

When we are talking how to use ‘wait()’ method, we should know how the retry-ability of cypress works and we should know the three concepts:

  • Commands
  • Queries
  • Assertions

Just refer the cypress documentation link above to know what are the ‘Principles and key characteristics of’ commands, queries, assertions and of course what is a ‘non-query’.

Here is a sequence diagram of retries for command, query and assertion:

sequence diagram illustrating the retry logic for commands, queries, and assertions in Cypress

Configuring retries, timeout setting configuration:

  • In the cypress runner we can see the settings JSON, we can use these properties as per requirements and AUT. 
  • In the case of ci-cd, we need to have these in our ‘cypress.config.js’ by default we don’t have all these properties in our config.js file.
Cypress configuration file
image1 1

Conditional based testing:

Dos and Donts:

  • End to End testing strategy should be in a determined manner
  • We should create the test scripts and business/user flows which will return determined results.
  • So before applying any conditional-based testing scenarios, we should be sure of the state of the elements
    most of the applications today are highly dynamic and mutable.
    their states and the DOM are continuously changing over a period of time . If the state of the DOM is highly unpredictable, we should not go for a conditional approach, which will result in more test flakiness.

Cypress has provided dedicated documentation for conditional testing.


Version compatibility [cypress core & plugins]

  • We all know that all extended functionalities rely on the plugins of Cypress.
  • In order to maintain a stable project, we must ensure the version we are using for Cypress and its plugins are in sync / would support the expected functionalities.

Issues:

  • As Cypress evolves, some features may be deprecated in favor of new approaches. 
  • Plugins relying on these deprecated features may not function properly without updates.

Best Practice/ Solution:

  • Stay updated with the latest versions and change logs on the details of the functionalities added and if some are deprecated like that.
  • Consider locking down the versions of Cypress and its plugins to ensure stability

Conclusion:

By incorporating these considerations into the creation and maintenance of our Cypress e2e testing framework, we can achieve greater robustness, eliminate flakiness, and facilitate seamless CI/CD integration.

Also, it is an iterative process, we should inspect our approach and adapt it according to the project requirements and priorities.