6 Cypress Best Practices for Making Your Tests More Deterministic

6 Cypress Best Practices for Making Your Tests More Deterministic

With code examples
Ferenc Almasi β€’ Last updated 2022 May 26 β€’ Read time 6 min read
  • twitter
  • facebook

Writing good E2E tests can be harder than it seems. You have to come up with the right configurations, the right ways to use your test suite, and decide on what to test and what to not, and how to test those things that you do need tests for. So here are 6 different best practices you can follow when writing your tests to get the most out of Cypress. In fact, you can apply the below practices to any testing framework, not just for Cypress.

Learn Cypress with Educative

1. Use data Attributes When Selecting Elements

One of the most important best practices you can follow when creating your E2E tests is writing selectors that are completely isolated from your CSS or JavaScript. You want to create selectors that can be specifically targeted for testing purposes so that no CSS or JavaScript update can break your test suite, just because a selector has been changed. The best option here is to use custom data attributes:

Copied to clipboard! Playground
// βœ… Do
cy.get('[data-cy="link"]');
cy.get('[data-test-id="link"]');

// ❌ Don't
cy.get('button');          // Too generic
cy.get('.button');         // Coupled with CSS
cy.get('#button');         // Coupled with JS
cy.get('[type="submit"]'); // Coupled with HTML

These clearly describe their purpose, and you will know that if you change them, you also need to update your test cases.

In summary: Avoid using class names, ids, tags, or common attribute selectors. Use custom data selectors to isolate your test from your CSS and JS.


2. Set a Base Url

Setting a base URL globally is also a great practice. It can not only make your tests cleaner, it also makes it easier to switch the test suite between different environments, such as a localhost and a production site.

Copied to clipboard! Playground
// βœ… Do set a base URL in your cypress.json config
cy.visit('webtips/cypress');

// ❌ Don't
cy.visit('https://webtips.dev/webtips/cypress');
cy.visit('http://localhost/webtips/cypress');

When it comes to Cypress, this also has performance benefits. By default, if you don't define a global base URL, Cypress will try to load in your localhost before switching to the final location when it comes across a cy.visit command.

In summary: Set a base URL to avoid unnecessary reloads and easily switch between different environments.

Looking to improve your skills? Check out our interactive course to master Cypress from start to finish.
Master Cypressinfo Remove ads

3. Avoid Using cy.wait with a Number

A common pitfall in Cypress is using the cy.wait command with a fixed number. This likely happens because you want to wait for an element to show up or a network request to finish before proceeding. In order to prevent random failures, you introduce cy.wait with an arbitrary number to ensure that commands have finished running.

The problem with this is that you end up waiting more than necessary. If you use cy.wait with 5000 milliseconds to wait for a network request, but the request finishes in 500 milliseconds, then you increased the run time of your test suite by 4500 milliseconds for no reason.

Copied to clipboard! Playground
// βœ… Do
cy.intercept('POST', '/login').as('login');
cy.wait('@login'); // Waiting for the request explicitly

// ❌ Don't
cy.wait(5000);

Instead, use cy.wait with an alias to ensure that the condition you are waiting on is met, so you can proceed safely. You can also use assertions in place of cy.wait to ensure that certain conditions are met before moving on.

In summary: Only use cy.wait with an alias to avoid waiting more than necessary.


4. Tests Should be Able to Pass Independently

Another common mistake that you can do is creating tests that are coupled and depend on each other. Relying on the state of a previous test leads to a brittle test suite that can break the rest of your test cases if initial conditions are not met. Take the following as an example:

Copied to clipboard! Playground
// ❌ Don't
it('Should log the user in', () => { ... });
it('Should be able to change settings', () => {
    cy.get('[data-cy="email"]').type('email@updated.com');
});

it('Should show updated settings', () => {
    cy.contains('[data-cy="profile"]', 'email@updated.com');
});

In the above code example, each test relies on the previous one, meaning that if one fails, others will too. If you change things in the first one, you likely have to update the rest. Decouple your tests and either combine multiple steps that rely on each other into one, or create shared code that you can reuse.

In summary: Tests should be able to pass independently from each other, never rely on the state of a previous test.


5. Control State Programmatically

Whenever you need to set the state for your application so you can test under the right conditions, always try to set the state programmatically, rather than using the UI. This means your state will be decoupled from the UI. You will also see a performance improvement, as setting the state programmatically is faster than using the UI of your application.

Copied to clipboard! Playground
// βœ… Do
cy.request('POST', '/login', {
    email: 'test@email.com',
    pass: 'testPass'
});

// ❌ Don't
cy.get('[data-cy="email"]').type('test@email.com');
cy.get('[data-cy="pass"]').type('test@email.com');
cy.get('[data-cy="submit"]').click();

In the above code example, we can use cy.request to directly communicate with an API to log a user in, rather than using the UI to do the same. The same applies to other actions, such as adding test data for your application to get it into the right state.

In summary: Whenever you need to set the state for the right conditions, do so programmatically rather than using the UI of your application.


6. Avoid Single Assertions

Last but not least, avoid using single assertions. Single assertions might be good for unit testing but here we are writing E2E tests. Even if you don't separate your assertions out into different test steps, you will know exactly which assertion failed.

Copied to clipboard! Playground
// βœ… Do
it('Should have an external link pointing to the right domain', () => {
    cy.get('.link')
      .should('have.length', 1)
      .find('a') 
      .should('contain', 'webtips.dev');
      .and('have.attr', 'target', '_blank');
});

// ❌ Don't
it('Should have a link', () => {
    cy.get('.link')
      .should('have.length', 1)
      .find('a');
});

it('Should contain the right text', () => {
    cy.get('.link').find('a').should('contain', 'webtips.dev');
});

it('Should be external', () => {
    cy.get('.link').find('a').should('have.attr', 'target', '_blank');
});

Most importantly, Cypress runs lifecycle events between your tests that reset your state. This is more computation-heavy than adding assertions to a single test. Therefore, writing single assertions can have a negative impact on the performance of your test suite.

In summary: Avoid using single assertions, to prevent unnecessary state resets that affects performance.

Want to learn Cypress from end to end? Check out my Cypress course on Educative where I cover everything:

Learn Cypress with Educative
Cypress Best Practices
If you would like to see more webtips, follow @flowforfrank

  • twitter
  • facebook
Did you find this page helpful?
πŸ“š More Webtips
Mentoring

Rocket Launch Your Career

Speed up your learning progress with our mentorship program. Join as a mentee to unlock the full potential of Webtips and get a personalized learning experience by experts to master the following frontend technologies:

Courses

Recommended

This site uses cookies We use cookies to understand visitors and create a better experience for you. By clicking on "Accept", you accept its use. To find out more, please see our privacy policy.