Cypress For Test Automation: A Step-by-Step Guide

cypress testing guide

With the complexity of software applications growing every day, one of the most important things for every testing team to focus on is optimizing test coverage. Testers all over the world use testing tools and frameworks to conduct various kinds of testing. However, testing software systems from end-to-end along with all of their subsystems is an urgent necessity.

Cypress is a modern JavaScript-based E2E (End-to-End) testing framework designed for modern web applications. It provides a powerful and easy-to-use way to automate tests. It is primarily used for testing web applications and can be integrated with various tools and frameworks like React, Angular, Vue, and more. Its unique features, like time travel and automatic waiting, can significantly improve the efficiency and reliability of your testing process.

In this blog, we will deep dive into how to set up Cypress to perform End to End testing with Cypress locally and in the cloud using TestGrid

Before explaining more about Cypress let’s see the NPM trend for Cypress. Looking at the npm trends give us some insights into the popularity and adoption rates of Cypress.

NPM trend for Cypress

About Cypress Testing Framework

Cypress is a popular open-source testing framework that is primarily used for web application testing. It is built using JavaScript and operates directly within the browser.

One of the key advantages of Cypress over other testing frameworks like Selenium is that it doesn’t require the use of driver binaries. The screenshot below shows how tests are executed inside of the browser, which reduces test execution time and removes network latency.

image2
Source

Cypress automation testing runs on a NodeJS server. To create a connection between the browser and the Node.js server, Cypress uses WebSocket protocol. WebSockets provide full-duplex communication which allows Cypress to send commands, such as navigating to URLs, interacting with elements, and making assertions, from the test code to the browser in real-time. It also enables Cypress to receive feedback, such as DOM snapshots, console logs, and other test-related information, from the browser back to the server.

Let’s break down the components and how they interact:

User Interaction: The process starts with a user interacting with the web application. This could involve actions like clicking buttons, selecting values from drop down, filling forms, or navigating through pages.

Cypress Test Scripts: Developers create test scripts using JavaScript or Typescript. These scripts simulate user interactions and verify that the application behaves as expected.

Cypress Runner: The Cypress Runner executes the test scripts. It interacts with the web application, capturing screenshots and videos during the test.

Proxy Server: A proxy server sits between the Cypress Runner and the web application. It intercepts requests and responses, allowing developers to manipulate them.

Node.js: Cypress runs on Node.js, providing a runtime environment for executing JavaScript/or Typescript code.

Web Socket: Web sockets protocol enables real-time communication between the Cypress Runner and the web application.

HTTP Requests/Responses: In the architecture diagram you can see HTTP requests (e.g., GET, POST) and responses exchanged between the Cypress Runner and the application server.

How Cypress Test Execution Works:

  • The user interacts with the web application.
  • Cypress test scripts send commands to the Cypress Runner.
  • The Cypress Runner communicates with the proxy server.
  • The proxy server forwards requests to the application server.
  • The application server processes requests and sends back responses.
  • The Cypress Runner captures screenshots and videos during the test.
  • Developers analyze the test results to ensure the application functions correctly.

So far we have seen how test cases are executed internally in Cypress. In the next section, you will see why Cypress is the best choice for automation.

Why Cypress For Automation Testing

Cypress has become very popular in the automation testing space due to several reasons:

Easy Setup: Cypress has a simple installation process and does not require any additional dependencies or setup. It is simple to include into both new and current projects.

Automatic Waiting: Cypress automatically waits for the elements to appear on the page before performing any actions or assertions. It intelligently handles asynchronous actions, eliminating the need for explicit waits or sleep statements.

Real-Time Reloads: As you make changes to your test files or application code, Cypress automatically reloads the test runner, providing real-time feedback. Debugging and development are sped up by this functionality.

Time Travel: Cypress allows you to go back and forth in time during test execution. You can take snapshots at different points in your test and even manipulate the application’s state to debug failures more effectively.

Interactive Test Runner: Cypress provides a user-friendly test runner interface that displays the application being tested in real-time. You can interact with the application, inspect elements, and see assertions and command logs.

Debuggability: Cypress offers built-in debugging tools that allow you to pause test execution at any point and inspect the application’s state. You can also use the browser’s developer tools alongside Cypress for advanced debugging.

Network and XHR Control: Cypress provides full control over the network and XHR requests. You can stub or mock network requests, modify responses and test various scenarios without making actual HTTP calls.

Cross-Browser Testing: Cypress supports cross-browser testing and allows you to run tests on different browsers simultaneously. It also provides plugins for integration with popular Continuous Integration (CI) platforms and cloud-based testing services.

Getting Start with Cypress Testing

Install Cypress

To start with Cypress testing we need to set up Cypress first. To create a new project for Cypress automated testing, we have to follow the below steps

Install Node.js: Cypress requires Node.js to run. Download and install Node.js from the official website (https://nodejs.org) if you haven’t already. Ensure you have Node.js version 16 or above.

Create a new project: Open your terminal or command prompt and navigate to the directory where you want to set up your Cypress project. 

Create a project, let’s name it cypress_testgrid

Initialize a new Node.js project: Start a new Node.js project by entering the commands below, and a package.json file will be generated.

npm init -y
Getting Start with Cypress Testing

Install Cypress: Run the below  command to install Cypress as a development dependency:

npm install cypress –save-dev

OR

yarn add cypress –dev

In the below screenshot, you can see Cypress is installed successfully

image1

One cypress is installed you can see the latest version in package.json, as of today when I am writing this article the latest version of cypress is 13.7.1

image22

In the above screenshot, we can see Cypress is installed. Now run the below command to open the Cypress runner and execute the test cases.

yarn cypress open

OR

npx cypress open 

You will see the below screen, select E2E Testing from the below screenshot.

Cypress Dashboard screenshot

Follow the wizard and finally, you will see the screenshot below with default test cases provided by Cypress.

image21

Folder structure of Cypress Framework

The below screenshots demonstrate how Cypress creates a default folder hierarchy when it first launches.Below is a detailed description of each of these files and folders that Cypress framework have.

Folder structure of Cypress Framework

Below table summarizes the purpose of each directory and file within the Cypress project.

Directory/FileDescription
cypress/Main directory containing all Cypress-related files.
fixtures/This directory contains JSON files that store static test data. For example, you can place sample data for API responses or form inputs in these files.
e2e/In the e2e directory, your test files (also known as spec files) reside. You can organize your tests into folders based on the application’s features or modules.
support/The support folder contains the files commands.js and e2e.js.command.js: The commands.js file is commonly used to define custom commands that you can reuse across different test filese2e.js: The e2e.js file is the entry point for loading various support files..
Cypress.config.jsCypress uses a configuration file ‘cypress.config.json’  to alter a project’s default setup settings.
node_modulesThis directory contains the dependencies installed for your Cypress project. It is automatically generated when you run npm install or use a package manager to install Cypress.
package.jsonHolds metadata about the project and its dependencies.

In next section you will see how we can create new test cases in Cypress

Create Test File in Cypress

Create a new test file under cypress/e2e/  to run e2e test case ,let’s give name to the file e.g, loginAndTabFunctionality.cy.js

Create Test File using Cypress

Write Your Test

As we mentioned above Cypress supports the Mocha framework. Therefore, prior to moving to our initial script, it is crucial to grasp certain key concepts.

Describe() Block : 

describe() block is to define a suite of tests that are logically related to each other. This could be tests for a specific module, component, or functionality of an application.

Describe() Block in cypress

it() Blocks:  

it() block defines a single, specific test case. This test case focuses on verifying a particular aspect of the functionality you’re testing.

it() block cypress

Hooks:

Cypress leverages Mocha’s hooks (before(), beforeEach(), after(), and afterEach()) to provide a structured way to execute code before or after tests.

The order of execution for these hooks is crucial:

  • before(): Runs once before all tests in a test file. Ideal for one-time setup tasks like connecting to a database or initializing global variables.
  • beforeEach(): Runs before each individual test. Useful for setting up test-specific conditions or data, ensuring each test starts from a clean slate.
  • afterEach(): Runs after each individual test. Good for cleaning up after each test, like closing database connections or clearing temporary data or logout from the application etc.
  • after(): Runs once after all tests in a test file. Use this for final cleanup tasks that may need to happen only once after all tests are complete.
Cypress uses Mocha's hooks

Assertions:

Cypress provides its own set of built-in assertions and extends Mocha with Chai assertions that offer a convenient way to verify test conditions. These assertions use a chainable syntax, making your tests more readable.

Some of the Examples are  .should(‘have.text’, ‘Expected Text’),.should(‘not.equal’, ‘Expected Text’),.should(‘exist’),.should(‘not.be.disabled’),  .should(‘have.class’, ‘active’), etc.

cypress Assertions

Read more about Cypress assertions in this post.

Excluding the Tests

The .skip() method allows you to temporarily exclude a test or an entire test suite from being run during test execution.

cypress .skip() method to execute the test

The describe.skip() function allows you to temporarily exclude an entire suite of tests defined within a describe block from execution during test runs.

describe.skip() function syntex in cypress

Including the Tests:

The .only() method allows you to instruct the test runner to execute only the specific test or test suite marked with .only(). All other tests in the suite or file will be skipped.

describe.only() method in cypress

So far we discussed some concepts that are required for a user who is writing his script for the first time. Let’s leverage these concepts we’ve discussed to build a simple Cypress script.

Automate Your First Cypress Test Case

Let’s create a new folder under the e2e folder named  “cypress_testgrid_testcases” to perform Cypress end-to-end testing.

Let’s create a new folder under the e2e folder named  “cypress_testgrid_testcases” to perform Cypress end-to-end testing.
Let’s take cypress testing examples to test end to end test scenarios from login to the site https://testgrid.io/ with valid credentials. Verify tab functionality and logout from the application.

Test Scenario


1. Visit the Site https://testgrid.io/
2. Login into the site valid credential 
3. Verify the user is logged in by varying the text  “Dashboard”
4. Click on the ‘Codeless’  link under the Automation section.
5.Verify the text “Let’s get you started with codeless automation”
6.Open The Link ‘Real Device Cloud’ in the New Tab and then Back To the Parent Page
7.Verify text “Selenium” to make sure user back to parent page

Write a simple Cypress test in file ‘loginAndTabFunctionality.cy.js’ which covered below functionality

  • Visiting the website https://testgrid.io/
  • Login into the site with valid data
  • Verify tabbing functionality.
/// <reference types="cypress" />
describe("Verify Login/Logout And Tab functionality", () => {
 beforeEach(() => {
   cy.visit("https://testgrid.io/")
   cy.get('[title="Sign in"]').click()
   cy.get("#email").clear("xxxxxx@gmail.com").type("xxxxxx@gmail.com");
   cy.get("#password").clear().type("xxxxx");
   cy.get(".signin-button").click();
 });
 afterEach(() => {
   cy.get("[data-toggle='dropdown']").click();
   cy.contains("Logout").click();
   cy.contains("Forgot Password?");
 });
 it("Login and Click on 'Codeless' link Under Automation Section", function () {
   cy.contains('Dashboard')
   cy.get("#tgtestcase").click();
   cy.contains("Lets get you started with codeless automation");
 });
 it("Open The Link 'Real Device Cloud' in New Tab and Back To Parent Page", function () {
   cy.get('[id="realdevice_li" ] > a').invoke("removeAttr", "target").click();
   cy.url().should("include", "/devicecloud");
   cy.contains('Device Groups')
   cy.go("back");
   cy.contains("Selenium");
 });
});

Code walkthrough

To understand the cypress testing example script in detail. Let’s undertake the above code walkthrough.

Before Each Test (beforeEach):

  • Visit the TestGrid.io homepage (cy.visit(“https://testgrid.io/”)).
  • Clicks on the “Sign in” button (cy.get(‘[title=”Sign in”]’).click()).
  • Enters the email address “xxxxx@gmail.com” in the email field (cy.get(“#email”).clear().type(“jxxxxx@gmail.com”)).
  • Enters the password “xxxxx” in the password field (cy.get(“#password”).clear().type(“xxxxx”)).
  • Clicks on the “Sign in” button (cy.get(“.signin-button”).click()).

After Each Test (afterEach):

  • Clicks on the user dropdown menu (cy.get(“[data-toggle=’dropdown’]”).click()).
  • Clicks on the “Logout” option (cy.contains(“Logout”).click()).
  • Verifies the presence of the text “Forgot Password?” after logout  for confirmation user is logged-out

Test 1: Login and Click on ‘Codeless’ Link

  • Verifies the presence of the text “Dashboard” after login (likely to confirm successful login).
  • Clicks on the element with ID “tgtestcase”
  • Verifies the presence of the text “Lets get you started with codeless automation” to confirm navigating to the codeless automation section

Test 2: Open Link in New Tab and Go Back

  • Clicks on the link text within the element with ID “realdevice_li” and removes the “target” attribute using invoke (likely to open the link in a new tab – cy.get(‘[id=”realdevice_li” ] > a’).invoke(“removeAttr”, “target”).click()).
  • Asserts that the URL includes “/devicecloud” to confirm navigation to the “Real Device Cloud” page
  • Verifies the presence of the text “Device Groups” on the new page.
  • Uses cy.go(“back”) to navigate back to the previous page (TestGrid homepage).
  • Verifies the presence of the text “Selenium” after returning to the homepage of TestGrid.

We have written our FIRST script now time to execute the script locally. In the next section you will see how we can execute the test script locally.

Execute The Test Cases Locally

To execute the test cases, you can use Cypress commands in your terminal. Cypress provides options to run tests both in headed mode (where you can see the browser window) and headless mode (where tests run in the background without a visible browser window). 

Here’s how you can execute the test cases in both modes:

Run Test Cases In Headed Mode:

In headed mode, you can see the test execution in a visible browser window.

  • Open your terminal.
  • Navigate to the directory where your Cypress tests are located.
  • Run the following command to execute the tests in headed mode.

Run the command  yarn cypress open   Or  npx cypress open .  as we run the command it will open below screen

cypress dashboard

Click on E2E testing and select the browser and finally execute the test case from the list, in our case we are executing the test case ‘loginAndTabFunctionality.cy.js

cypress test execution

When we click on loginAndTabFunctionality.cy.js test case starts executing and finally BOTH the test case pass successfully.

Below are the screenshots of the test case execution locally.

image19
image14
test case execution locally on cypress
image5

Run Test case In Headless Mode

By default, Cypress runs in “headless mode,” where the browser runs in the background without opening a visible browser window.

Command to run the test case in headless mode  yarn cypress run   Or  npx cypress run

Run the command: 

‘yarn cypress run –spec cypress/e2e/cypress_testgrid_testcase/loginAndTabFunctionality.cy.js’. As you execute this command, the test case starts executing in headless mode. 

In the below screenshot you can see once execution is completed where BOTH the test cases are passed.

cypress headless

Cypress Best Practices 

Every tool has some best practices that can help maximize its effectiveness and efficiency. Below are a few best practices of Cypress.

Avoid Unnecessary Wait :

Suppose you have a scenario where you want to test the visibility of an element after clicking on a button. Instead of using a static cy.wait() command, which introduces unnecessary waiting time, you can use Cypress commands like cy.get() with assertions to wait for the element to become visible.

describe('Avoid Unnecessary Waits Example', () => {
   it('should check visibility after button click', () => {
     // Visit the webpage containing the button and element
     cy.visit('https://example.com')
      // Click on the button
     cy.get('#myButton').click()
      // Instead of using cy.wait(), wait for the element to become visible
     cy.get('#myElement')
       .should('be.visible')
       .and('have.text', 'Expected Text')
   })
 })

Set baseUrl In Cypress configuration File

Hard-coding the baseUrl using cy.visit() in the before() block of each spec file is not the best approach . With the baseUrl set in the configuration file, Cypress will automatically prepend this base URL to any relative URLs used in your tests. 

Here’s how you can use cy.visit() with a relative URL in your test files:

For example, in your cypress.config.js file, you can set the baseUrl to the login page URL

const { defineConfig } = require("cypress");

module.exports = defineConfig({
 e2e: {
   "baseUrl": "https://example.com",
   experimentalStudio:true,
   setupNodeEvents(on, config) {
     // implement node event listeners here
   },
 },
});
describe('Login Page Test', () => {
   it('should load the login page', () => {
     // Cypress will automatically prepend the baseUrl to this relative URL
     cy.visit('/login')
      // Add your test assertions here
   })
 })

Multiple assertions per test Cypress

Writing a single assertion in a test can make it run more slowly and lead to problems with performance. The recommended practice is to add multiple assertions with a single command, which speeds up testing and improves test clarity and organization. In the below example you can see we have multiple validation in a single command.

cy.get('.element')
 .should('have.class', 'active')
 .and('be.visible')
 .and('have.text', 'Hello World');

Isolate it() blocks

Isolating it() blocks is indeed a best practice in Cypress (as well as in other testing frameworks). Each it() block should be independent and self-contained, with its own setup and teardown steps. This ensures that tests can run independently of each other, making them more robust and reliable.

Here’s how you can isolate it() blocks in Cypress:

describe('Test Suite', () => {
   beforeEach(() => {
     // Common setup steps before each test
     cy.visit('/'); // Navigate to the page
     // Other setup steps as needed
   });
    afterEach(() => {
     // Common teardown steps after each test
     // Cleanup tasks, if any
     // We can add logout test case here 
 
   });
    it('Test Case 1', () => {
     // Test steps for the first scenario
     cy.get('.element').should('have.class', 'active');
     cy.get('.element').should('be.visible');
   });
    it('Test Case 2', () => {
     // Test steps for the second scenario
     cy.get('.other-element').should('have.text', 'Hello World');
     cy.get('.other-element').click();
     // Additional assertions and interactions
   });
 });

Keeping test data separate

Keeping test data separate is indeed a best practice in Cypress. Separate data files are easier to update and manage, especially when dealing with a lot of test data. You can reuse the same test data across multiple tests if applicable

Cypress allows you to define test data in external JSON files called fixtures. This keeps your test code clean and separates concerns between test logic and test data.

// Example fixture file: test-data.json
{
   "user": {
     "username": "testuser",
     "password": "password123"
   }
 }
// Example usage in a test
beforeEach(() => {
  cy.fixture('test-data.json').then((data) => {
    this.userData = data.user;
  });
});

Also, read this detailed article to Level up your Cypress testing with these best practices!

Conclusion 

This blog post provided a comprehensive introduction to Cypress, a modern and powerful end-to-end testing framework. It covered the fundamentals of Cypress, its benefits, and best practices for writing and executing test cases efficiently. With its ease of use and robust features, Cypress is an excellent choice for automating web applications and ensuring a seamless user experience.