App & Browser Testing Made Easy

Give your users a seamless experience by testing on 3000+ real devices and browsers. Don't compromise with emulators and simulators

Home Guide Cypress Best Practices for Test Automation

Cypress Best Practices for Test Automation

By Hamid Akhtar, Community Contributor -

Cypress is a cutting-edge front-end testing tool designed for the modern web. You could easily claim that Cypress solves the major issues that QA engineers and developers have while testing modern applications. 

Cypress vs Selenium is topic that is frequently contrasted, however, Cypress is fundamentally and architecturally distinct from Selenium. Cypress is not subject to the same limitations that apply to Selenium. As a result, you can construct tests more quickly, easily, and correctly.

Users of Cypress are often programmers or quality assurance engineers creating online applications with modern JavaScript frameworks. Anything that is browser-based can be tested by Cypress.

You may write all different kinds of tests using Cypress:

  • End-to-end tests
  • Component tests
  • Integration tests
  • Unit tests

A locally installed, free and open source application called Cypress and Cypress Cloud for recording your tests make up the entire ecosystem of Cypress.

First, as you construct your application locally, Cypress assists you in setting up and writing tests every day. The greatest of TDD! Cypress Cloud can record your test runs once you’ve generated a suite of tests and integrated Cypress with your CI Provider. 

Never will you have to wonder why something failed!

9 Cypress Best Practices you Need to Know

Use data Attributes When Selecting Elements

One of the most important best practices you can adopt when creating your E2E tests is writing selectors completely independent of your CSS or JavaScript. In order to prevent your test suite from being broken by a simple CSS or JavaScript update, you should construct selectors that may be explicitly targeted for testing. 

Utilising custom data attributes is the best choice in this case:

// ✅ 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 explicitly state what they are intended to accomplish, and you will be aware that if you change them, your test cases will also need to be updated.

It is difficult to select an element using id and class because these attributes are largely used for behaviour and styling, which means they are constantly subject to change. It’s likely a bad idea to do this if you don’t want the tests that come out of it to be brittle. 

Use data-cy or data-testid instead wherever possible. Why? They are more dependable because they are created purely for testing and are therefore independent of the behaviour or styling.

Let’s say, for instance, that we have an input element:

<input
id="main"

type="text"
class="input-box"

name="name"
data-testid="name"
/>

To target this element for testing, use data-testid rather than id or class:

// Don't ❌
cy.get("#main").something();
cy.get(".input-box").something();
​
// Do ☑️
cy.get("[data-testid=name]").something();

Independent it() blocks

In it() block, we write the test case script. In a spec file, we include several it() blocks inside the “describe” block. In this scenario, it is essential that we follow the coding principle that no two it() blocks of code should depend on one another.

This approach of coding was selected because, when we run a spec file with many it() blocks (for instance, 10 test cases) if one of them fails, the other it() blocks won’t fail either because there is no dependency between them.

Using dynamic wait

Some developers write code for any page action, such as visiting a URL, saving, updating, or deleting action, and then waiting for the API to be active and return results using the cy.wait(timeout) command.

cy.visit('/')
cy.wait(5000) // <--- this is unnecessary

The script will wait for five seconds when the aforementioned code is executed, even when the page loads in just two or three seconds.

Using the static wait command cy.wait(timeout) while writing code in these situations is not recommended.

A better fix is to use cy.intercept() while writing dynamic wait code.

cy.intercept('POST', '**/login').as('login');
cy.visit("/")
cy.wait('@login')

In the code above, we utilize cy.wait() to hold off on using the specific API “login.” The next piece of code begins to run once we receive the API’s result. The main benefit of developing dynamic code is that it speeds up the script execution and reduces waiting time.

Cypress basics: before(), beforeEach(), after() and afterEach()

Consider that each of the 10 test cases in our specification file needs to begin with a few lines of code that are applicable to all of them. In this case, starting each it() block with the same repetitive lines of code is not a smart idea. 

The answer is to write the code in the beforeEach() hook

The beforeEach() hook will automatically execute whatever code it contains before each test case is executed.

In a similar way, the afterEach() hook may be used to write common code that will execute following the completion of each and every test case. Additionally, we can use the before() hook to write a piece of common code that executes before all test cases in a spec file are executed. The after() hook allows us to write common code that will execute following the execution of all test cases in a spec file.

Adding BaseUrl in the config file

The login page is the base URL of the application in the majority of cases. The login URL must be used as the base URL in all spec files in order to perform logins and other test-related activities. Some programmers use cy.visit() to hard code the base URL in each spec file’s before() block, as seen in the example below:

before(() => {
cy.visit("https://demoapp.com")
})

This is a bad tactic because when the spec file is executed in the Cypress runner, it will first load the localhost URL and then reload the URL we supplied in the cy.visit.

This takes time and appears unprofessional. By entering the base url as shown below in the cypress.json file, this issue can be resolved.

cy.visit("http://localhost:3000/login")
Change it to:
cy.visit("/login")
cypress.json
{
...
"baseUrl": "http://localhost:3000"
...
}

Defining “scripts” in package.json

Typically, we use Visual Studio Code’s terminal to execute Cypress commands. To open the Cypress runner, for instance, we’ll use the “cypress open” command. “npm run cypress open” is the terminal command to use. It is recommended to include this “cypress open” command in the package.json file’s “scripts” JSON section.

“npm run cy:open” is the corresponding command we type in the terminal. The “scripts” JSON allows us to define all the commands we use to run in the terminal together with a user-defined name.

The main advantage of creating in this method is that, when we need to run lengthy commands in the terminal, we may define the command in “scripts” JSON and utilize the user-defined name.

3rd Party Servers

It’s possible for your application to have an impact on another application developed by a third party. While not frequently occurring, these circumstances are nevertheless conceivable. Consider integrating your app with GitHub so that users can edit data inside of GitHub using the app.

You can use cy.request() to programmatically communicate with GitHub’s APIs after your test has finished running rather than attempting to cy.visit() GitHub. This eliminates the need to ever interact with another application’s user interface.

Control State Programmatically 

In order to test under the appropriate conditions, try to set the state of your application programmatically whenever you can rather than through the UI. As a result, the UI will no longer be dependent on your state.

You will also notice an improvement in performance because the programmatic state setting is quicker than using the UI of your application.

// ✅ 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();

Instead of using the UI to do the same task, as seen in the code sample above, we can utilize cy.request to communicate directly with an API to log a user in. This also holds true for other actions, like adding test data to your application to put it in the proper state.

Avoid Single Assertions

Avoid using single assertions. While single assertions may work well for unit testing, we are writing E2E tests here. You will be able to identify the specific assertion that failed even if you don’t divide your Cypress assertions up into multiple test phases.

// ✅ Do
it('Should have an external link pointing to the right domain', () => {
cy.get('.link')
.should('have.length', 1)
.find('a') 
.should('contain', 'wtips.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', 'wtips.dev');
});

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

The most significant part is that Cypress runs lifecycle events between your tests that reset your state. This requires more processing than simply adding assertions to one test. As a result, writing a single assertion may hinder the effectiveness of your test suite.

Is Cypress redefining Test Automation?

Yes. Cypress developed a ton of amazing features that we have been seeking in every test automation solution for a very long time.

  • Installed. Initialized. And you can now begin creating automated tests to ensure that your application’s most critical functionalities are covered. Such simple! Cypress even offers executable demonstrations of how to use each feature to help you examine the many scenarios provided by modern web apps.
  • Time travel: Cypress collects snapshots while your tests are running. No automation tool currently has this feature, with the exception of Cypress. Cypress captures snapshots of your application and lets you go back in time to the moment when commands were executed. 
  • The test script’s debugging is never easy. You may save time on debugging with Cypress. Clear error notifications make faster debugging possible. The pinned Snapshot displays an element’s prior and subsequent states. All of the developer tools for the browser are also available to you.
  • With Cypress, numerous tools that are already well-known to the JavaScript community are packaged for use. These tools include jQuery, Moment, the Sinon library for mocks, and Lodash with its many features, as well as the assertion libraries Mocha and Chai.
  • Retry-ability is a key component of Cypress that helps with testing dynamic web applications. It enables the tests to end each command as soon as the assertion passes, without requiring waits to be hard-coded. No major concern, the test doesn’t need to alter at all if your application renders each DOM element in a few milliseconds or even seconds. Cypress automatically waits for commands and assertions and claims before continuing.
  • By default, Cypress does not attempt to communicate with elements until they are visible. It automatically waits for requests to finish before commencing the testing. Even animations take longer than expected to finish.
  • By using functions like cy.intercept(), you can intercept HTTP requests, assign them an alias, and wait for them to complete before continuing with the tests. This makes the tests more reliable.
  • Cypress uses a more effective approach to locating web elements than Selenium using its cy.get (element value), which renders the elements more quickly than Selenium because it does not use explicit waits. In addition to achieving the purpose of automation, it enables developers to write front-end unit tests.
  • Spies, Stubs, and Clocks: Verify and maintain control over how functions, server responses, or timers behave. You have access to the same features that you love about unit testing.

Cypress Best Practices for Test Automation

Run Cypress Tests in Parallel

With BrowserStack cloud infrastructure, teams can carry out extensive Cypress testing and opt for hassle-free Cypress parallelization. Depending on the parallelization parameters you’ve provided, automatically split your spec files and perform the tests on the given browsers & OS.

Run Cypress Tests at Scale

Closing Notes

  • Your tests will run much more quickly and smoothly without ever producing any errors or failures if you follow these Cypress best practices. 
  • If properly applied and recommended practices are followed, Cypress is a great testing framework.
  • It could be annoying or challenging to put some of Cypress’s best practices into effect. 
  • However, they will undoubtedly pay off in the long run and help you save a ton of time when carrying out Cypress E2E testing
Tags
Automation Frameworks Cypress

Featured Articles

Cross Browser Testing with Cypress : Tutorial

Cypress Automation Tutorial

Curated for all your Testing Needs

Actionable Insights, Tips, & Tutorials delivered in your Inbox
By subscribing , you agree to our Privacy Policy.
thank you illustration

Thank you for Subscribing!

Expect a curated list of guides shortly.