Cypress-javascript-Advance
Best Practices Cypress Cypress for Web UI Automation Testing

Maximizing Test Efficiency with Cypress: Best Practices for Fast, Reliable Results

Efficient test automation is key to ensuring smooth software testing, and Cypress provides a range of powerful features to help. This blog will explore best practices that enhance the performance and maintainability of your test automation efforts. We’ll look at organizing test suites for better structure, using Cypress fixtures to manage test data, and leveraging custom commands to boost reusability. You’ll also learn how to handle timing issues using Cypress’s retry mechanism and incorporate API testing to expand your test coverage.

Additionally, we’ll cover parallel test execution for faster results, testing across multiple viewports and devices, and CI/CD integration to ensure continuous delivery. We’ll also focus on optimizing test performance and dealing with flaky tests, providing practical tips to increase the reliability and speed of your tests.

Table of Contents

🎯Choosing Best-Suited Locators in Cypress

Selecting the right locators for elements is crucial for building stable, reliable, and maintainable test automation scripts in Cypress. Efficient locators help minimize the chances of flaky tests, which can break when there are changes in the web application, such as the structure or attributes of HTML elements. Cypress offers several ways to locate elements, and choosing the best locator depends on the scenario and the structure of the application under test.

Here are the most common types of locators used in Cypress, along with tips and examples on when and how to use them.

1️⃣Using data-* Attributes

data-* attributes (e.g., data-test-id) are one of the best practices for creating stable locators in Cypress. These attributes are added specifically for testing purposes, and they remain unaffected by UI changes, ensuring that the test scripts remain stable over time.
Use data-* attributes when the development team can collaborate and include these attributes in the HTML specifically for testing.

Example: <button data-test-id=”submit-btn”>Submit</button>

Cypress Command: cy.get(‘[data-test-id=”submit-btn”]’).click();

Since data-* attributes are intended solely for automation, they are less likely to change and won’t interfere with UI development. It makes the locator strategy resilient to front-end changes.

2️⃣Using Unique IDs

Using unique id attributes is another reliable method to locate elements in Cypress. HTML id attributes are supposed to be unique within a page, making them a strong candidate for element identification.

Example: <input type=”text” id=”username” />

Cypress Command: cy.get(‘#username’).type(‘testUser’);

IDs are unique, making them reliable for locating elements. However, they might be less flexible compared to data-* attributes if the design changes frequently.

3️⃣Using Class Selectors

Classes are commonly used for styling purposes, and while they can be used as locators in Cypress, they might change frequently during UI updates. It’s important to ensure that the class names you use for locators are stable.
Use class selectors when you need to target multiple elements with the same class or when no better locators (like data-* or id) are available.

Example: <button class=”btn primary”>Submit</button>

Cypress Command:  cy.get(‘.btn.primary’).click();

4️⃣Using cy.contains() for Text-Based Locators

 Cypress provides the cy.contains() method to locate elements based on visible text. This is particularly useful for buttons, links, or elements with dynamic classes where the text is the only stable property. Use cy.contains() when the text inside the element is unique and reliable.

Example: <button class=”submit”>Submit Order</button>

Cypress Command: cy.contains(‘Submit Order’).click();

Text-based locators are generally stable and are often unchanged in the application. However, be careful when using this approach in multilingual applications where the text could change depending on the language.

5️⃣Using Attribute Selectors

Cypress allows you to locate elements by attributes other than id, class, or data-*. Attributes like name, type, or aria-label can be useful when other locators are not available.
Use attribute selectors when you cannot rely on data-*, id, or text and when attributes like name or type are consistent.

Example: <input type=”text” name=”email” />

Cypress Command: cy.get(‘input[name=”email”]’).type(‘user@example.com’);

Attribute selectors are flexible, and as long as they don’t change frequently, they can be an efficient way to locate elements. However, they’re not as reliable as data-* attributes.

6️⃣Using DOM Traversal Methods

Sometimes, there is no direct attribute that you can use to locate an element. In such cases, you can rely on DOM traversal methods like parent(), find(), and children() to navigate the HTML structure.

Use DOM traversal methods when you need to find an element relative to another element.

Example:

<div class="form">

  <label for="email">Email</label>

  <input type="text" id="email" />

</div>

 Cypress Command: cy.get(‘.form’).find(‘input’).type(‘test@example.com’);

Traversal methods are helpful when dealing with elements nested within complex structures. They ensure that your locator is flexible enough to adapt to changes in the DOM structure, but they may require more maintenance.

🎗️Best Practices for Choosing Locators in Cypress

  • Prioritize data-* Attributes:
    • Collaborate with developers to add data-test-id attributes for critical elements in your application. These attributes offer the most stability.
  • Use Unique IDs Sparingly:
    • IDs are often reliable but should only be used when guaranteed to be unique across the page.
  • Avoid Over-reliance on Class Names:
    • Classes are primarily for styling and may change. If using class selectors, ensure the class names won’t be refactored often.
  • Use Text-Based Locators When the Text is Unique:
    • cy.contains() is handy for buttons, links, or other elements that have consistent, meaningful text.
  • Avoid XPath:
    • While XPath is supported in Cypress, it is often more complex and harder to maintain than CSS selectors. Stick to CSS selectors for simplicity and better performance.

Real-World Example

Imagine you’re working on automating an online banking application. In the application, there’s a button that appears based on different user roles. Each button looks the same but appears under different conditions, making it challenging to identify with typical locators like class or id. You can solve this by asking developers to add data-test-id attributes to each button.

<button data-test-id="submit-payment-btn" class="btn btn-primary">Submit Payment</button>
<button data-test-id="approve-loan-btn" class="btn btn-primary">Approve Loan</button>

cy.get('[data-test-id="submit-payment-btn"]').click();

By following this approach, your locators are clear, maintainable, and less prone to breaking when minor UI changes occur.

Using a global baseUrl in Cypress

Simplifying test management starts with flexibility. In Cypress, a global baseUrl can save you time and effort by preventing hard-coded URLs in test files. This approach ensures easier maintenance and seamless adaptability across various environments like development, staging, and production.

Setting Up baseURL in Cypress

The baseUrl is configured in Cypress’s cypress.config.js file. By setting the baseUrl, Cypress can automatically prepend this URL to any relative URL used in your tests. This makes tests cleaner, reusable, and easier to update when environments change.

Example : In cypress.config.js, define the baseUrl:

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

module.exports = defineConfig({
  e2e: {
    baseUrl: 'https://jignect.tech/', // Replace with your desired URL
    setupNodeEvents(on, config) {
      // implement node event listeners here
    },
  },
});

Once the baseUrl is set, you can use relative paths in your tests without specifying the entire URL.

Test Example: Here’s a simple test before and after using a baseUrl

1️⃣ Without baseUrl

describe('Test with full URL', () => {
  it('should visit the qa-services page, () => {
    cy.visit('https://jignect.tech/qa-services/');
    cy.contains('Exploring the World of Software Testing').should('be.visible');
  });
});

2️⃣With baseUrl

describe('Test using baseUrl', () => {
  it('should visit the homepage', () => {
    cy.visit('about-us/');
    cy.contains('About JigNect').should('be.visible');
  });
});

In this second version, since the baseUrl is already defined, you only need to specify the relative URL (about-us/). This makes your tests cleaner and easier to maintain, especially if the baseUrl changes.

Benefits of Using baseUrl

  1. Cleaner Test Code: No need to repeat the full URL in every test.
  2. Easier Environment Switching: By changing the baseUrl in one place, you can switch between development, staging, or production environments.
  3. Better Maintainability: When the URL structure changes, updating the baseUrl in a single configuration file updates all related tests.
  4. Reduced Redundancy: Avoid redundant full URL paths that clutter the codebase.

Real-World Example

Imagine you’re testing an e-commerce platform with multiple environments for different regions. By setting a dynamic baseUrl per environment, you can easily run the same test suite across dev, staging, and production without modifying the test logic.

For instance, in a global e-commerce site where the base URLs are region-specific (e.g., us.example.com, eu.example.com), you can leverage the baseUrl configuration for each region.

module.exports = defineConfig({
  e2e: {
    baseUrl: 'https://us.example.com', 
  },
});

Organizing Test Suites for Maintainability

Organizing test suites properly is fundamental to creating maintainable, scalable, and easy-to-debug test automation. As your test cases grow, having a well-structured test suite ensures that future changes can be easily made, and the test code remains readable for anyone in the team. A well-organized test suite can help streamline testing workflows, avoid duplication of tests, and simplify troubleshooting when something goes wrong.

Key Practices for Organizing Test Suites

Similar tests should be grouped logically. This ensures that each test suite or file handles a particular section of the application, such as login functionality, product searches, or checkout processes. This segregation helps prevent overlap between tests and makes it easier to identify which part of the application is causing failures.

2️⃣Use Descriptive Test Names and Folders

Test files and folder names should clearly indicate what functionality is being tested. Instead of vague names like test1.js, use more specific names such as user-login.spec.js or cart-management.spec.js. This practice makes the test suite self-explanatory and easier to navigate.

3️⃣Apply Test Tags or Categories

Some testing frameworks, including Cypress, allow you to tag or categorize tests. This can be useful for running specific groups of tests, such as regression tests, smoke tests, or tests related to a particular feature or module. Tagging helps testers run only the relevant tests when required, improving efficiency.

4️⃣Modularize Setup and Teardown Code

To avoid redundancy, any repetitive test setup or teardown logic should be centralized in functions or before/after hooks. This ensures that your tests remain clean and focused on actual test steps, making the suite easier to maintain over time.

Real-World Example

Suppose you’re working on an e-commerce platform that has features like user login, product search, shopping cart, and checkout. As the number of test cases grows, it becomes harder to manage everything in one big test file. To make things easier, you can organize your test suites like this:

1️⃣ Folder Structure

cypress/
├── integration/
│   ├── auth/
│   │   ├── login.spec.js
│   │   ├── signup.spec.js
│   ├── products/
│   │   ├── product-search.spec.js
│   │   ├── product-details.spec.js
│   ├── cart/
│   │   ├── add-to-cart.spec.js
│   │   ├── remove-from-cart.spec.js
│   └── checkout/
│       ├── checkout-flow.spec.js
├── fixtures/
├── plugins/
├── support/
└── cypress.json

2️⃣ Grouped Test Files

For example, in the auth/ folder, you have login.spec.js and signup.spec.js. These test files are solely focused on the login and signup flows. Each file contains tests related to its specific functionality, and each is independent, making them easier to maintain and debug.

3️⃣Test Tags Example

If you want to run only smoke tests (i.e., high-priority tests for quick feedback), you can tag them in the code:


// In login.spec.js
describe('User Login', { tags: ['smoke', 'auth'] }, () => {
  it('should log in with valid credentials', () => {
    cy.visit('/login');
    cy.get('#username').type('validUser');
    cy.get('#password').type('validPassword');
    cy.get('#loginBtn').click();
    cy.url().should('include', '/dashboard');
  });
});

Now, you can filter and run just the smoke or auth tests when you need to perform quick validations.

4️⃣Centralized Setup Code Example

If the login step is common across multiple tests, you can modularize it by adding a custom command in the support/commands.js file:

Cypress.Commands.add('login', (username, password) => {
  cy.visit('/login');
  cy.get('#username').type(username);
  cy.get('#password').type(password);
  cy.get('#loginBtn').click();
});

In your test files, instead of repeating the login steps, you can just call cy.login(‘validUser’, ‘validPassword’) in each test. This ensures that if the login page changes, you only need to update the login code in one place, making it easier to maintain.

Benefits of Organized Test Suites

  • Easier Maintenance: By grouping related tests and organizing the structure properly, testers can quickly locate the necessary test cases to modify, debug, or run.
  • Reduced Redundancy: Centralized setup, teardown, and reusable commands prevent duplicate code, making future changes simpler.
  • Scalability: As more tests are added, the structured suite can scale without becoming unmanageable.
  • Faster Debugging: Since failures are isolated in specific test files or suites, identifying and fixing problems becomes much quicker.

Using Cypress Fixtures for Managing Test Data

Managing test data in automation can be tricky, especially when the data keeps changing. Cypress, a popular testing tool, offers a helpful solution with its fixture feature. Fixtures allow you to keep test data in separate files and use them in your test scripts, making everything more organized and reusable.

What Are Fixtures in Cypress?

Fixtures are separate files, usually in JSON format, that store your test data. These files prevent you from putting data directly into your tests, making them easier to manage. Cypress automatically looks for fixture files in the cypress/fixtures folder, keeping your test scripts neat and tidy.

Real-World Example: Imagine you’re testing an online store where users can sign up and log in. Instead of typing in user details for each test, you can store this data using Cypress fixtures and load it as needed. This method makes managing data simpler and more efficient.

Step-by-Step Example:

1️⃣Create a Fixture File

{
  "newUser": {
    "username": "test_automationuser",
    "email": "test.automationuser@yopmail.com",
    "password": "password123"
  },
  "existingUser": {
    "username": "automation_user",
    "email": "automation.user@yopmail.com",
    "password": "password456"
  }
}

2️⃣ Use the Fixture in a Test

In your test file, you can load the data from this fixture and use it in the test:

describe('User Registration and Login Tests', () => {
  beforeEach(() => {
    // Load the fixture before each test
    cy.fixture('users').as('userData');
  });

  it('should register a new user', function () {
    cy.visit('/register');
    cy.get('#username').type(this.userData.newUser.username);
    cy.get('#email').type(this.userData.newUser.email);
    cy.get('#password').type(this.userData.newUser.password);
    cy.get('#register-button').click();
    
    // Check if registration was successful
    cy.contains('Welcome, test_automationuser');
  });

  it('should log in an existing user', function () {
    cy.visit('/login');
    cy.get('#username').type(this.userData.existingUser.username);
    cy.get('#password').type(this.userData.existingUser.password);
    cy.get('#login-button').click();
    
    // Check if login was successful
    cy.contains('Welcome back,automation_user ');
  });
});

Benefits of Using Fixtures

  1. Cleaner Tests: By keeping the data in fixture files, your tests look cleaner and are easier to understand.
  2. Reuse Data: The same fixture file can be used in different tests, making it easier to manage data across multiple test cases.
  3. Easy Maintenance: If you need to change the data, you only need to update the fixture file instead of updating each test case.
  4. Better Organization: Storing data separately from test logic makes it easier to scale your tests as they grow.

Leveraging Custom Commands for Reusability in Cypress

In Cypress, test cases frequently involve repeated actions that must be done in many tests, like logging in, filling out forms, or moving through certain pages. Instead of copying the same steps in each test case, Cypress lets you make custom commands. These custom commands turn these repeated tasks into reusable functions, following the DRY (Don’t Repeat Yourself) rule. This makes your test suite tidier, more efficient, and simpler to manage.

Why Is It Important?

  • Code Reusability: You write the code once and use it wherever needed.
  • Maintainability: If a change is required (e.g., login flow is updated), you only need to update the custom command, and all tests that use it will automatically reflect the change.
  • Readability: Tests become cleaner and more readable since you are abstracting complex steps into simple, descriptive commands.

Example: Let’s take a common scenario: logging into a web application.

Without Custom Command

You might have a login function that repeats in every test file like this:

describe('Login Test Suite', () => {
  it('should login successfully', () => {
    cy.visit('/login');
    cy.get('input[name="username"]').type('testuser');
    cy.get('input[name="password"]').type('password123');
    cy.get('button[type="submit"]').click();
    cy.url().should('include', '/dashboard');
  });
});

Now, imagine you have 10 different tests in your suite that need to perform the same login action. Copy-pasting this code across multiple test files leads to redundancy, which is difficult to maintain if the login flow changes.

With Custom Command

Cypress provides a way to define custom commands in a reusable manner. You can abstract the login logic into a custom command by adding it to the commands.js file:

Create a Custom Command: In the cypress/support/commands.js file, add the custom login command.

Cypress.Commands.add('login', (username, password) => {
  cy.visit('/login');
  cy.get('input[name="username"]').type(username);
  cy.get('input[name="password"]').type(password);
  cy.get('button[type="submit"]').click();
});

Using the Custom Command in Tests: Now, in your test files, you can simply call the custom command cy.login().

describe('Login Test Suite', () => {
  it('should login successfully', () => {
    cy.login('testuser', 'password123');
    cy.url().should('include', '/dashboard');
  });
});

Benefits:

  • The login action is abstracted into a single place.
  • If the login page changes (e.g., a new form field is added), you only need to update the custom command in commands.js, and it will reflect across all tests using cy.login().

Utilizing Cypress’s Built-in Retry Mechanism

Cypress has a built-in feature that helps make test automation easier and more reliable. This feature automatically tries actions and checks until they either succeed or fail. This is really helpful in situations where things might not be ready right away or could change, like on websites that load content slowly.

Important Point:

Cypress automatically keeps trying commands like cy.get(), cy.click(), or checks like should(). It keeps trying until it either works or until a set time limit (4 seconds by default) is reached, and then it stops the test. This means you don’t have to write extra code to keep trying things, which makes your tests simpler and more consistent.

Real World Example: Imagine you’re checking a to-do list app. You want to make sure that when a user adds a task, it shows up in the list. But because of slow internet or screen effects, the task might not appear right away on the screen.

Here’s how Cypress’s retry mechanism handles this:

describe('To-Do App', () => {
  it('should add a task to the list', () => {
    // Visit the app
    cy.visit('https://example.com/todo');

    // Type the task into the input field and hit enter
    cy.get('input.new-task').type('Buy groceries{enter}');

    // Assert that the new task appears in the list
    cy.get('.task-list')
      .should('contain', 'Buy groceries');
  });
});

In this test, Cypress will retry the cy.get(‘.task-list’) and the associated should() assertion until the text “Buy groceries” appears or until the timeout is reached. This ensures the test doesn’t fail due to slight delays in rendering the new task.

How It Helps in Real Life?

In real-world applications, UI elements often take some time to load due to backend processing or animation delays. Without a retry mechanism, you’d need to add arbitrary wait() commands or implement custom retry logic, which can make tests harder to maintain and more prone to failure. Cypress’s automatic retries remove this complexity, improving both the stability of tests and the speed of execution.

Best Practices

  • Leverage Automatic Retries: Use Cypress’s retries wherever possible. Avoid unnecessary wait() commands.
  • Understand Timeout Settings: If elements take longer to load, consider adjusting the default timeout for specific cases using cy.get({ timeout: 10000 }) for example, but avoid setting large global timeouts to prevent long delays.
  • Focus on Stability: Design tests in a way that handles asynchronous events and dynamic changes in the UI gracefully by using Cypress’s retry mechanisms.

🔀Assigning Return Values in Cypress

In Cypress, you often need to capture the result of an action or a value from the DOM to use it in subsequent steps within your test. Cypress commands are asynchronous, so the values cannot be assigned directly in the same way as synchronous code. Instead, you need to handle these values using Cypress’s chaining mechanism or .then() command to extract the data.

Why is This Important?

Cypress commands like cy.get() or cy.request() return promises, and their actual values (like the text of an element, the response of an API, etc.) are not available immediately. Properly handling these return values is critical for writing tests that validate the behavior of your application effectively.

Example 1: Assigning Return Value of an Element’s Text

Let’s say you want to capture the text of a DOM element and use it later in your test.

Scenario: You are testing an e-commerce website where you need to capture the price of an item and validate it after adding the item to the cart.

describe('E-commerce Item Price Validation', () => {
  it('should capture item price and validate after adding to cart', () => {
    // Navigate to the item page
    cy.visit('/product-page');

    // Capture the price of the item
    cy.get('.item-price').then(($price) => {
      const priceText = $price.text(); // Return value assigned

      // Perform some action (like adding the item to the cart)
      cy.get('.add-to-cart-btn').click();

      // Now validate the price in the cart
      cy.get('.cart-item-price').should('have.text', priceText);
    });
  });
});

Explanation:

  1. The cy.get(‘.item-price’) fetches the price element from the DOM.
  2. .then(($price) => {…}) is used to access the value of the selected element. Inside this block, we extract the text value from $price using .text().
  3. We store this value in priceText and then use it later to validate that the price in the cart matches the captured price.

Key Points to Remember:

  • Return values in Cypress are accessed asynchronously using .then() or chaining commands.
  • Avoid using direct variable assignments because Cypress commands return promises, and you need to handle them in a callback.
  • Aliases (as()) are useful for sharing values across test steps within a single test.

Defining “scripts” in “package.json” in Cypress

In Cypress, the package.json file plays an important role in managing the dependencies and configurations of your project. One of the most useful features of this file is the ability to define “scripts.” These scripts are simple commands that can be executed via the command line, automating various tasks like running tests, linting code, or cleaning up the environment.

By defining scripts in the package.json, you can easily run your Cypress tests and manage the workflow more efficiently without having to remember long or complex commands.

Why Define Scripts in package.json?

  • Simplifies Commands: Instead of typing out long commands, you can define scripts and run them with a simple npm run command.
  • Improves Readability: It helps keep your testing workflow clear and manageable.
  • Enables Automation: Easily integrate Cypress tests into CI/CD pipelines by using defined scripts.

How to Define Cypress Scripts in package.json

In the package.json file, the scripts section is where you define custom scripts. Here’s an example of how to define Cypress-related scripts:

{
  "name": "my-cypress-project",
  "version": "1.0.0",
  "description": "A project for Cypress testing",
  "scripts": {
    "test": "cypress open",
    "cypress:run": "cypress run",
    "cypress:run:headless": "cypress run --headless",
    "cypress:run:spec": "cypress run --spec 'cypress/integration/spec_file.js'",
    "cypress:run:chrome": "cypress run --browser chrome"
  },
  "dependencies": {
    "cypress": "^12.0.0"
  }
}

Explanation of the Cypress Scripts

  1. test
  • The test script is often the default script in most Node.js projects. Running npm test or npm run test will launch the Cypress Test Runner interface.
  • Command: npm test
  • Use Case: This is beneficial for testers who want to interactively run their tests through the Test Runner, allowing them to observe the tests as they execute in real-time.
  1. cypress:run
  • This script runs all the Cypress tests in headless mode. This is ideal for CI/CD environments where you want the tests to run without requiring a UI.
  • Command: npm run cypress:run
  • Use Case: Use this script to automate testing in headless mode, which is perfect for automated build pipelines (e.g., Jenkins, GitLab CI).
  1. cypress:run:headless
  • Similar to cypress:run, but explicitly runs the tests in headless mode (without opening a browser window).
  • Command: npm run cypress:run:headless
  • Use Case: Ideal for automated builds where you want tests to run silently without opening any browser.
  1. cypress:run:spec
  • This script allows you to run a specific test file rather than the entire test suite. This is particularly useful when you are working on or debugging a specific feature.
  • Command: npm run cypress:run:spec
  • Use Case: Run this when you need to test only a specific file, such as during development or when troubleshooting.
  1. cypress:run:chrome
  • By default, Cypress runs tests in Electron, but you can specify a different browser, such as Chrome, using this script.
  • Command: npm run cypress:run:chrome
  • Use Case: Run this script when you need to verify that your tests work  in a different browser, such as Chrome. This is crucial when ensuring cross-browser compatibility.

Real-World Example of Using Scripts: Let’s consider an example of an e-commerce application where the team is running different sets of tests for development and production environments.

  • Development Environment: In the development environment, the team prefers to run tests using the Cypress Test Runner to debug their code visually.
    • Script Usage:
      • npm run test

This command opens the Cypress Test Runner, where the team can manually select and run the tests they need to work on.

  • Production Environment (CI/CD Pipeline): In the production CI/CD pipeline, the team wants to run tests headlessly to ensure everything works in a fully automated environment.
    • Script Usage:
      • npm run cypress:run:headless

The tests are executed without launching a browser, and the results are logged into the pipeline system (e.g., Jenkins or GitLab CI). This reduces execution time and integrates smoothly into the deployment workflow.

Customizing Cypress Run Configurations

You can also customize your test runs by adding more flags or options to the scripts in package.json.

Run Cypress in headless mode with specific environment variables:

"scripts": {
  "cypress:run:prod": "cypress run --headless --env configFile=prod"
}
  • Command: npm run cypress:run:prod
  • Use Case: This script is useful for running tests in the production environment configuration without opening the browser.

Independent it() Blocks in Cypress with package.json Configuration

In Cypress, every test block is written inside an it() function, which represents a single test case. By default, each it() block runs independently, but some settings or coding practices can create dependencies between these blocks, which can cause issues when tests are run in isolation or in different environments. It’s important to configure tests so that each it() block remains truly independent and self-contained.

What Are Independent it() Blocks?

Independent it() blocks are test cases that do not depend on the outcome of other tests to run successfully.Each test should work independently, without needing the results or conditions from any other test. This is important in automated testing to make sure tests are reliable and don’t produce inconsistent results.

Cypress, by default, executes each it() block independently. However, you can encounter issues if you unintentionally create dependencies, such as sharing state between tests or relying on a specific execution order.

Key Practices for Ensuring Independent it() Blocks

  • Avoid Shared States Across Tests: Ensure that each it() block operates independently by avoiding shared states between tests. This can include data stored in variables, session cookies, or local storage that could affect the behavior of other tests.
  • Use beforeEach() and afterEach() Hooks: These hooks are essential for test setup and teardown. By using beforeEach(), you can set up the environment, such as logging into an application or resetting data, ensuring that each test starts from a clean state. afterEach() can be used to clear any leftover state.
  • Control Test Execution Order: Although Cypress does not guarantee a specific execution order for it() blocks, you should avoid writing tests that rely on a specific order. Each test should be capable of running in isolation, making test execution order irrelevant.

Example of Independent it() Blocks

describe('User Dashboard Tests', () => {

  beforeEach(() => {
    // Set up the state before each test (e.g., login, clean slate)
    cy.visit('/login');
    cy.get('input[name="username"]').type('testuser');
    cy.get('input[name="password"]').type('password');
    cy.get('button[type="submit"]').click();
  });

  afterEach(() => {
    // Tear down any shared state after each test (e.g., logout)
    cy.clearCookies();
    cy.clearLocalStorage();
  });

  it('should display the correct user dashboard', () => {
    cy.visit('/dashboard');
    cy.get('h1').should('contain', 'Welcome, testuser');
  });

  it('should allow the user to update their profile', () => {
    cy.visit('/profile');
    cy.get('input[name="email"]').clear().type('newemail@example.com');
    cy.get('button[type="submit"]').click();
    cy.get('.success-message').should('contain', 'Profile updated successfully');
  });
});

Modifying package.json for Cypress Test Independence

In some cases, you may want to enforce test isolation via the package.json configuration, particularly in CI/CD pipelines where tests may be executed in parallel or in batches.

{
  "scripts": {
    "test": "cypress run --headed --no-exit --parallel"
  },
  "cypress": {
    "baseUrl": "http://localhost:3000",
    "env": {
      "resetDB": true
    }
  }
}

In this configuration, Cypress is run with the –parallel flag, which ensures that test files can be split and executed independently across multiple environments or CI/CD runners. Running in parallel enforces that each test file and block (it()) does not rely on another, ensuring truly independent tests.

Best Practices:

  • Always reset the state before each test.
  • Ensure test cases do not depend on each other.
  • Utilize beforeEach() to prepare for tests and afterEach() to clean up.
  • Leverage the –parallel option for parallel test execution, ensuring each test runs in isolation.

Using after or afterEach hooks

In Cypress, the after and afterEach hooks are extremely useful for managing tasks that need to be executed after tests have run, such as cleanup or resetting the state for the next test. Here’s how you can use these hooks with a real-world example.

afterEach Hook in Cypress

The afterEach hook runs after each test case. It’s ideal for scenarios where you want to perform some action, such as resetting data or cleaning up after every individual test, ensuring a consistent environment for the next test.

Example: Logging Out After Each Test

In an e-commerce web application, where multiple tests might require login, you can use the afterEach hook to log out the user after each test.

describe('E-Commerce App - User Operations', () => {
  beforeEach(() => {
    // Logging in before each test
    cy.visit('/login');
    cy.get('#username').type('user1');
    cy.get('#password').type('password123');
    cy.get('#loginButton').click();
  });

  it('should add an item to the cart', () => {
    cy.visit('/shop');
    cy.get('.item').first().click();
    cy.get('#addToCart').click();
    cy.get('.cart-count').should('contain', '1');
  });

  it('should view order history', () => {
    cy.visit('/orders');
    cy.get('.order-item').should('exist');
  });

  afterEach(() => {
    // Logging out after each test
    cy.get('#logoutButton').click();
    cy.url().should('include', '/login');
  });
});

In this example, after each test (whether it’s adding an item to the cart or viewing order history), the user is logged out automatically using the afterEach hook. This ensures that each test starts with a clean session, without relying on previous test data or session states.

after Hook in Cypress

The after hook runs once after all the tests in a block have finished. It’s useful for performing tasks like final cleanup or resetting states after the entire test suite has completed.

Example: Clear Session After All Tests

In a scenario where you are testing various user functionalities, you might want to clear local storage or reset the database once all the tests are done.


describe('E-Commerce App - User Operations', () => {
  beforeEach(() => {
    // Logging in before each test
    cy.visit('/login');
    cy.get('#username').type('user1');
    cy.get('#password').type('password123');
    cy.get('#loginButton').click();
  });

  it('should add an item to the cart', () => {
    cy.visit('/shop');
    cy.get('.item').first().click();
    cy.get('#addToCart').click();
    cy.get('.cart-count').should('contain', '1');
  });

  it('should view order history', () => {
    cy.visit('/orders');
    cy.get('.order-item').should('exist');
  });

  after(() => {
    // Clear session after all tests
    cy.clearCookies();
    cy.clearLocalStorage();
    cy.log('All tests finished, clearing session data.');
  });
});

Here, the after hook is used to clear cookies and local storage once all the tests in the suite are completed. This is especially useful for scenarios where you don’t want any leftover session data that could affect future test runs.

When to Use after vs. afterEach

  • Use afterEach when you need to reset the application state after each individual test, ensuring that no test depends on the outcome of the previous one.
  • Use after when you need to perform cleanup or actions only once after all tests have finished running.

These hooks help maintain test independence and ensure that each test runs in isolation, which is crucial for writing reliable, efficient automation tests in Cypress.

Visiting external websites

In Cypress, visiting external websites (those that don’t share the same domain as the one under test) can be a bit tricky due to Cypress’s built-in security model. Cypress enforces a Same-Origin Policy, meaning that you can’t visit multiple domains within a single test, as this can lead to security vulnerabilities.

Cypress allows navigation to different URLs within the same domain (same-origin) easily, but when it comes to navigating to external websites or multiple domains, it introduces a limitation that you need to work around carefully.

However, there are strategies to visit external websites by splitting your test into multiple scenarios, or using workarounds such as visiting external sites at the start of your test.

Example: Suppose you are testing an e-commerce platform where users might be redirected to an external payment gateway such as PayPal or Stripe. Cypress won’t allow you to directly navigate from your e-commerce domain to the PayPal or Stripe domain because of the Same-Origin Policy.

Here’s how you could handle visiting external websites within Cypress tests:

// Test scenario for visiting an external website
describe('Handling External Websites in Cypress', () => {

  it('Should navigate to PayPal for checkout', () => {
    
    // Step 1: Visit the main e-commerce website (origin: https://myecommerce.com)
    cy.visit('https://myecommerce.com/checkout');

    // Simulate clicking the button to proceed with payment through PayPal
    cy.get('button.checkout').click();

    // Step 2: External URL redirection (origin: https://www.paypal.com)
    // This is where Cypress will fail due to its security policy

    // Workaround: Instead of testing PayPal directly, we can verify the redirection URL is correct
    cy.url().should('include', 'https://www.paypal.com');
    
    // Further interactions on the external website should be handled with API stubbing or mocking

  });

});

Explanation :

  • Primary Domain: Cypress allows visiting pages within the same domain, like https://myecommerce.com. You can freely navigate different paths like /checkout, /products, etc.
  • External Site (Blocked by Cypress): When Cypress tries to navigate to an external domain like https://www.paypal.com, the test will fail due to Cypress’s security restrictions.
  • Workaround: Instead of trying to interact with the external site directly, you can validate that the redirection URL is correct by checking the cy.url() method. This ensures that the user is being directed to the proper external site (e.g., PayPal), even though you can’t interact with that site directly in Cypress.
  • API Stubbing: In many cases, instead of testing interactions with an external website, testers simulate these interactions by using API stubbing or mocking responses. For example, you could mock a successful payment response from PayPal in your test.

Real-World Use Case:

Imagine you’re testing an online shopping platform that redirects customers to an external payment processor like PayPal. Cypress won’t let you navigate to PayPal, but you can assert that your system correctly generates the PayPal redirection URL. Additionally, using API stubbing, you could simulate a successful payment response from PayPal, enabling you to test the full user journey without visiting the external domain.

Optimizing Test Performance / Unnecessary Waits in Cypress

When using Cypress to automate tests, it’s important to focus on making the tests run faster. If the tests aren’t optimized well, they can take longer to complete, which means you get feedback later. One common issue is the presence of unnecessary waits, which can significantly hinder performance. Cypress offers built-in mechanisms to handle these waits efficiently, allowing testers to improve test speed without compromising accuracy.

Understanding Unnecessary Waits

Unnecessary waits refer to the overuse of static waits or fixed delays in tests, such as cy.wait(5000) or hard-coded timeouts. These waits make the test execution halt for a specific time, regardless of whether the application is ready to proceed or not. This method is inefficient because the test may be forced to wait longer than needed if the page or element is already loaded.

In contrast, using Cypress’s more intelligent wait strategies can help optimize the test performance. Cypress automatically waits for elements to be available and retry actions, reducing the need for arbitrary wait times.

Example of Unnecessary Waits

Imagine you’re testing an e-commerce platform, and the application takes time to load the product catalog page. Instead of using a static wait like this:

cy.visit('/products');
cy.wait(5000); // Unnecessary static wait
cy.get('.product-item').should('be.visible');

Here, the test will always wait 5 seconds, even if the page loads in just 2 seconds. This approach adds unnecessary delays and reduces the overall efficiency of the test suite.

Optimizing with Dynamic Waits

A better approach is to use Cypress’s built-in retry and dynamic waiting capabilities. Cypress automatically waits for elements to appear, making static waits redundant. Using commands like cy.get() with assertions is more effective because Cypress will keep trying until the condition is met or a timeout occurs.

Here’s the optimized version of the previous example:

cy.visit('/products');
cy.get('.product-item', { timeout: 10000 }).should('be.visible'); // Cypress waits dynamically

In this case, Cypress waits for up to 10 seconds for the .product-item element to be visible but doesn’t wait unnecessarily if it appears sooner. This reduces the total test execution time.

Best Practices for Handling Waits in Cypress

  1. Avoid Static Waits: Avoid using fixed delays like cy.wait(3000) unless absolutely necessary. Instead, leverage Cypress’s built-in retry capabilities.
  2. Use Implicit Waiting: Commands like cy.get(), cy.contains(), and assertions automatically wait for elements to appear or conditions to be met.
  3. Leverage Network Stubbing: Use cy.intercept() to wait for network requests to finish, ensuring that actions dependent on those requests aren’t executed prematurely.
  4. Set Timeout Values for Complex Operations: For operations that might take longer (e.g., file uploads or large data loads), use timeout options in commands to ensure Cypress waits appropriately without relying on static waits.

By eliminating unnecessary waits and using Cypress’s intelligent wait strategies, you can optimize test performance, reducing execution time and improving overall efficiency.

Testing Across Multiple Viewports and Devices in Cypress

In today’s web testing, it’s essential to ensure that a website or web app performs seamlessly across various devices and screen sizes. Users interact with web apps on computers, tablets, and phones, each with unique screen dimensions. As testers, it’s our responsibility to verify that the user experience remains consistent, regardless of the device being used. Cypress, a powerful tool for testing web apps, simplifies this process by allowing testers to easily check how the app behaves and appears across different screen sizes.

Cypress Viewport Commands

Cypress has special commands like cy.viewport() that let you simulate different device sizes and orientations. Using these commands, you can change the browser’s window size during your tests to see how the app’s interface behaves on different screen sizes. This makes it easy to test how the app works on mobile devices, tablets, and desktops all in one set of tests.

Example of Cypress Viewport Usage:

describe('Responsive Testing Example', () => {
  it('Should display correctly on mobile, tablet, and desktop', () => {
    // Desktop viewport
    cy.viewport(1280, 720)
    cy.visit('https://example.com')
    cy.get('#navbar').should('be.visible')
    
    // Tablet viewport
    cy.viewport('ipad-2')
    cy.get('#navbar').should('be.visible')
    
    // Mobile viewport
    cy.viewport('iphone-6')
    cy.get('#navbar').should('not.be.visible') // On mobile, navbar is hidden or changes
    cy.get('#mobile-menu').should('be.visible') // Mobile-specific menu becomes visible
  })
})

Benefits of Viewport Testing

  • Improved User Experience: Testing different viewports ensures that users on all devices have a consistent and smooth experience.
  • Early Detection of Layout Issues: Catching layout bugs early in the development cycle saves time and effort when fixing responsive design issues.
  • Seamless Testing Across Devices: Cypress makes it easy to simulate multiple devices in a single test run, reducing the need for manual testing across various physical devices.

Parallel Test Execution for Faster Results in Cypress

In today’s fast-paced testing environment, efficiency is key to ensuring high-quality software. One effective strategy is running tests in parallel, which is an essential part of a flexible testing plan. Cypress, a powerful testing tool, enables testers to execute multiple tests simultaneously, allowing them to validate various features of the application across different devices or environments at the same time. This approach not only saves valuable time but also accelerates feedback, helping testers quickly identify and address any issues.

1️⃣Understanding Parallel Test Execution in Cypress

Parallel test execution allows Cypress to run multiple test files concurrently, splitting the test workload across several machines or browser instances. This approach is essential when dealing with large test suites, as it dramatically reduces the time required to execute all tests.

For example, if you have 100 test cases that would take an hour to run sequentially, splitting them across five machines can reduce the execution time to just 12 minutes.

How it Works:

Cypress manages parallel test execution using its Dashboard service, which offers intelligent test orchestration. This ensures that no tests are run twice, and test files are distributed evenly across available machines.

2️⃣Configuring Parallel Test Execution in Cypress

To enable parallel execution in Cypress, you’ll need to configure it through the Cypress Dashboard and your CI/CD pipeline.

Steps to Configure:

  • First, ensure you have a Cypress Dashboard project set up and your tests are running in CI.
  • Use the cypress run command with the –parallel flag in your CI pipeline. For example:

  cypress run –record –parallel –ci-build-id <build-id>

Ensure your CI pipeline is set up to spawn multiple machines (workers) that can run tests in parallel.

3️⃣Best Practices for Parallel Test Execution in Cypress

  • Organize Tests Properly: Ensure test files are independent of each other so that they can be run in parallel without dependencies.
  • Use Tags or Groups: Tag or group your tests based on priority or functionality, which makes it easier to split them intelligently across machines.
  • Optimize Test Suite Size: Avoid too many test files in one suite. If a suite takes too long, break it down into smaller, more manageable parts.
  • Monitor Test Performance: Use the Cypress Dashboard to monitor test execution and adjust the parallel configuration based on test completion times.

4️⃣Benefits of Parallel Execution in Cypress

  • Faster Feedback: Running tests in parallel helps to reduce test completion time significantly, giving quicker feedback to developers.
  • Scalability: As the test suite grows, you can simply increase the number of machines or workers running the tests.
  • CI/CD Integration: Parallel execution fits perfectly with modern CI/CD pipelines, ensuring that tests are part of the continuous delivery process.

Dealing with Flaky Tests in Cypress

What Are Unreliable Tests? Tests that sometimes work and sometimes don’t, even when nothing in the code or environment is changed, are called unreliable tests. These tests can badly affect the trust in automated test groups, making it hard to know if a failure is from a real problem or just the test itself. This is especially bad in CI/CD systems where test trust is very important.

Common Causes of Flaky Tests in Cypress

  • Timing Issues: Flaky tests often arise from poor synchronization between the application under test and the automated tests. For example, your test may attempt to interact with a page element before it has fully rendered or become interactive. In Cypress, even though it automatically waits for elements, incorrect handling of asynchronous behavior can still cause problems.
  • Dynamic Content: In modern web applications, content may change dynamically based on API calls or user interactions. If your test expects static content but the data is delayed or dynamically updated, it may fail unpredictably.
  • Network Latency: External factors like network issues or slow API responses can also lead to flaky tests, as they impact the timing of actions and assertions.

Cypress Strategies to Reduce Flaky Tests

1️⃣Use Proper Assertions:

Cypress automatically waits for elements to appear, but flaky tests can still occur if assertions are not well defined. It’s essential to use explicit, specific assertions instead of relying on broad ones.

Example: Instead of just asserting that an element exists, you can be more specific:

// Weak assertion that may lead to flakiness
cy.get('.notification').should('exist');

// Stronger, more specific assertion
cy.get('.notification').should('contain.text', 'Your changes were saved');

This makes your tests more predictable because you are checking for an exact state or content rather than just existence.

2️⃣Leverage Cypress Retry Mechanism

Cypress has a built-in retry mechanism that automatically retries assertions until they pass or a timeout is reached. You can make use of this by writing robust assertions that leverage this feature.

Example: Suppose you’re testing a form submission where an API call takes time to return. Instead of manually adding waits, Cypress will retry assertions:

cy.get('button[type="submit"]').click();
cy.get('.success-message').should('contain', 'Form submitted successfully');

Cypress will wait until the .success-message appears and contains the correct text, minimizing flakiness related to timing issues.

3️⃣Avoid Hardcoded Waits

Adding arbitrary cy.wait() commands can lead to flaky tests, especially when the wait time is either too short or unnecessarily long. Instead, use dynamic waits like cy.intercept() for API requests or Cypress’s automatic retry mechanism for elements.

Example:

cy.get('button[type="submit"]').click();
cy.wait(5000);  // Arbitrary wait time
cy.get('.success-message').should('be.visible');

Use cy.intercept() to wait for the actual API response before proceeding:

cy.intercept('POST', '/api/submitForm').as('formSubmit');
cy.get('button[type="submit"]').click();
cy.wait('@formSubmit');
cy.get('.success-message').should('be.visible');

This approach makes your tests more resilient to changes in network speed or server response times.

4️⃣Control Test Data and Environment

Unstable data can cause tests to become flaky. It’s essential to control the state of your data, ensuring it’s consistent across test runs. Cypress fixtures or mocks can help with this.

Example: If your test depends on specific data being returned from an API, you can mock the response using cy.intercept():

cy.intercept('GET', '/api/userDetails', {
  statusCode: 200,
  body: {
    name: 'Test_User',
    age: 28
  }
}).as('getUserDetails');

cy.visit('/profile');
cy.wait('@getUserDetails');
cy.get('.user-name').should('contain.text', 'Test_User');

This eliminates the dependency on live data and network responses, reducing test flakiness.

5️⃣Use Retries for Flaky Tests

Cypress provides a retry mechanism that can be configured globally or per-test. If a test occasionally fails due to external issues (e.g., network problems), retries can give it another chance to pass.

Example: Enable retries in cypress.json:

{
  "retries": {
    "runMode": 2,
    "openMode": 1
  }
}

This tells Cypress to retry failed tests up to 2 times when running in CI and once when running locally.

6️⃣Mock Network Requests

Flakiness caused by unreliable external APIs can be handled by mocking network requests. This ensures that tests remain stable regardless of external factors like server downtime or slow API responses.

Example: Instead of relying on real API responses, mock the network request:

cy.intercept('GET', '/api/data', {
  fixture: 'data.json'
}).as('getData');

cy.visit('/dashboard');
cy.wait('@getData');

This ensures the test gets consistent data, reducing flakiness.

Real-World Example: Imagine you’re testing an e-commerce site where products are dynamically loaded based on user filters. You might have tests that validate that the correct products appear after applying filters. A flaky test could arise if the API response is delayed or the product grid takes longer than expected to update. Using strategies like cy.intercept() for mocking API responses and Cypress’s retry mechanism ensures that your tests wait for the correct product grid update, reducing the chances of flakiness.

cy.intercept('GET', '/api/products?category=electronics', {
  fixture: 'electronics-products.json'
}).as('getProducts');
cy.get('button.filter-electronics').click();
cy.wait('@getProducts');
cy.get('.product-list').should('have.length', 5);

In this example, you control the data and wait dynamically for the API response, ensuring that the test won’t fail due to timing issues or data inconsistencies.

Conclusion

By following the best practices outlined in this blog, you can significantly enhance the effectiveness and efficiency of your Cypress test automation efforts. By organizing your test suites, using fixtures for data management, leveraging custom commands, and utilizing Cypress’s built-in features, you can create more reliable, maintainable, and scalable tests. Additionally, defining scripts in your package.json and ensuring independent it() blocks will further streamline your testing workflows and improve overall test quality.

Witness how our meticulous approach and cutting-edge solutions elevated quality and performance to new heights. Begin your journey into the world of software testing excellence. To know more refer to Tools & Technologies & QA Services.

If you would like to learn more about the awesome services we provide, be sure to reach out.

Happy testing! 🙂