Cypress Automation Tutorial

Learn Cypress Automation with help of examples and best practices. Enhance Cypress test automation with BrowserStack.

Written by Sujay Sawant Sujay Sawant
Reviewed by Bhumika Babbar Bhumika Babbar
Last updated: 21 November 2024 22 min read

Key Takeaways

  • Cypress is a web testing tool that runs tests directly in the browser, making it easier to see, debug, and fix issues faster.
  • Cypress supports end-to-end, component, API, and visual testing, helping teams test user flows and visual changes in one place.
  • Built-in debugging features like logs, screenshots, and videos help teams find and fix test failures faster.

Cypress Automation Tutorial

Cypress is one of the first tools I reach for when I want web tests that are easier to write, run, and debug. It runs directly in the browser, so I can see what the test is doing instead of guessing from logs after a failure.

If a button does not respond, an API takes too long, or a page changes unexpectedly, Cypress makes the failure easier to trace.

In this article, I’ll walk through Cypress automation, how it works, its key features, and how to use it for testing modern web applications.

What is Cypress Automation?

Cypress is a JavaScript-based test automation framework used to test modern web applications. It is mainly used for end-to-end testing, where testers verify complete user flows such as login, checkout, form submission, search, and navigation.

Unlike traditional testing tools that control the browser from outside, Cypress runs inside the browser with the application. This allows it to observe page changes, retry commands automatically, and give a clear step-by-step view of how each test runs.

Cypress is commonly used by developers and QA teams because it is easy to set up, simple to write tests in, and useful for debugging failures quickly.

How Cypress Works Across Each Testing Type

Cypress supports different types of testing depending on whether you want to validate a complete user journey, a single UI component, an API response, or the visual appearance of a page.

Testing TypeWhat it TestsHow Cypress Works
End-to-End TestingA complete user flow across the applicationOpens the app in a real browser and performs actions like a user
Component TestingA single UI component in isolationMounts the component directly and tests its behavior without running the full app flow
API TestingBackend responses and business logic exposed through APIsSends HTTP requests using cy.request() and validates the response
Visual TestingPage layout, UI appearance, and visual regressionsCaptures or compares screenshots using Cypress integrations or visual testing tools

Why Use Cypress For Automation?

Cypress solves many of the problems that make end-to-end tests slow, flaky, and difficult to debug.

  • Automatic waiting: Waits for elements, commands, and assertions without needing hard-coded delays.
  • Developer-friendly syntax: Uses readable JavaScript commands that are easy for developers and QA teams to maintain.
  • Real-time debugging: Shows each test step in the browser with command logs and time-travel snapshots.
  • Network control: Uses cy.intercept() to mock APIs, simulate failures, and test edge cases.
  • CI/CD support: Runs easily in automated pipelines with screenshots, videos, and test reports for debugging failures.

Cypress Architecture

Instead of sending commands to the browser from an external driver, Cypress runs with the application in the browser and controls it through its own runner, Node.js server, proxy layer, AUT iframe, and command queue.

This architecture is the reason Cypress can show every test step in real time, retry commands automatically, capture snapshots, and give detailed debugging information when a test fails.

Cypress Architecture

Runner, Node Server, AUT iframe, and Command Queue

The Cypress Test Runner is the interface you see when tests are running. It shows the list of test cases, command logs, snapshots, errors, screenshots, and browser output. When a command fails, the runner helps you inspect what the application looked like at that exact step.

The Node.js server runs in the background. It handles tasks that cannot happen directly inside the browser, such as reading configuration files, accessing the file system, loading plugins, managing screenshots and videos, and running setup logic through setupNodeEvents.

The proxy layer sits between the browser and the application. Cypress uses this layer to observe and control network traffic. This is what makes features like cy.intercept() possible, where you can spy on requests, mock responses, simulate failed APIs, or delay network calls.

The AUT iframe stands for Application Under Test iframe. Cypress loads your application inside this iframe while the Cypress runner stays outside it. This setup lets Cypress control the application, observe DOM updates, and display what is happening in the test runner at the same time.

The command queue controls how Cypress commands are executed. Commands like cy.visit(), cy.get(), cy.click(), and cy.type() do not run immediately when the test file is read. Cypress adds them to a queue and runs them one by one. During execution, it waits, retries, captures snapshots, and moves to the next command only when the current command is resolved.

How Cypress Test Execution Works Under the Hood

Cypress tests do not execute commands immediately as the JavaScript file is read. Instead, Cypress first builds a command queue and then runs each command in order. This is one of the most important things to understand when writing Cypress tests.

For example:

cy.visit('/login') 

cy.get('[data-cy="email"]').type('user@example.com') 

cy.get('[data-cy="password"]').type('password123') 

cy.get('[data-cy="login-button"]').click() 

cy.url().should('include', '/dashboard')

When Cypress reads this test, it queues each command first. Then it executes them step by step:

  1. Open the login page.
  2. Find the email field.
  3. Type the email address.
  4. Find the password field.
  5. Type the password.
  6. Click the login button.
  7. Check whether the URL includes /dashboard.

During this process, Cypress automatically waits for commands to complete before moving to the next one. If an element is not available immediately, Cypress retries the command until the element appears or the timeout is reached.

This is different from adding fixed waits like:

cy.wait(5000)

A fixed wait pauses the test for a set amount of time, even if the page becomes ready earlier. Cypress retryability is smarter because the test moves forward as soon as the expected condition is met.

Cypress also records each command in the Test Runner. This creates a visible timeline of the test, showing what Cypress clicked, typed, waited for, and asserted. If the test fails, you can inspect the failed command and see what the page looked like at that exact moment.

In simple terms, Cypress execution works like this:

StepWhat Happens
Test file is loadedCypress reads the test and identifies the commands
Commands are queuedCypress adds commands like cy.visit(), cy.get(), and cy.click() to a queue
Commands run in orderEach command waits for the previous command to finish
Cypress retries automaticallyQueries and assertions retry until they pass or time out
Results are recordedThe Test Runner logs each step with snapshots and errors

This execution model helps Cypress handle dynamic web applications without relying on unnecessary delays. It also makes failures easier to debug because every step is visible in the command log.

How Cypress Controls the Application

When you write a Cypress test, you are not directly controlling the browser in the same way Selenium does through WebDriver. Cypress runs its own automation code in the browser and communicates with the application as it renders and changes.

For example, when you write:

cy.get('[data-cy="login-button"]').click()

Cypress does not simply look for the button once and fails immediately if it is not found. It keeps retrying the query until the element appears or the command times out. Once the element is found and becomes actionable, Cypress performs the click and records that step in the Test Runner.

This retry behavior applies to many Cypress commands and assertions. It is one of the main reasons Cypress tests can handle dynamic interfaces without relying on fixed delays like cy.wait(5000).

Key Components of the Cypress Framework

With the architecture in place, these are the main parts you work with when writing Cypress tests:

Test Runner: Runs tests interactively, displays command logs, shows snapshots, and helps debug failures.

Spec Files: These contain the actual test cases written using Cypress commands and Mocha-style syntax such as describe(), it(), beforeEach(), and afterEach().

Cypress Commands: Commands like cy.visit(), cy.get(), cy.click(), cy.type(), cy.contains(), and cy.intercept() are used to interact with the app and validate behavior.

Assertions: Cypress supports assertions such as should() and expect() to verify page state, text, visibility, URLs, API responses, and more. Many assertions retry automatically until they pass or time out.

Fixtures: Fixture files store reusable test data, usually in JSON format. They are commonly used for form data, API mock responses, and repeatable test inputs.

Custom Commands: These allow repeated actions, such as login or form setup, to be moved into reusable commands like cy.login().

Configuration File: cypress.config.js or cypress.config.ts controls project settings such as base URL, viewport size, retries, timeouts, screenshots, videos, environment variables, and CI-specific behavior.

Together, these components make Cypress more than just a browser automation tool. It provides a full testing environment for writing, running, debugging, and maintaining automated tests for modern web applications.

Setting Up Cypress

Setting up Cypress is straightforward if your project already uses Node.js. Before installing Cypress, make sure the basic JavaScript environment is ready.

Prerequisites

Cypress runs on Node.js, so you need:

  • Node.js: Required to install and run Cypress
  • npm, yarn, or pnpm: Used to manage Cypress as a project dependency
  • Code editor: VS Code is commonly used for writing and debugging tests
  • Existing web project: Cypress can be added to React, Angular, Vue, Next.js, or any JavaScript-based web application

Install Cypress

To install Cypress using npm, run:

npm install cypress --save-dev

npm install cypress

This adds Cypress as a development dependency in your project. You can also install it using yarn or npm:

yarn add cypress --dev

npm add cypress --save-dev

Output –

npm-install-cypress

Open Cypress for the First Time

After installation, open Cypress with:

npx cypress open

Output –

npx-cypress-open

This launches the Cypress app. From here, you can choose the testing type, configure the project, select a browser, and start running tests.

For end-to-end testing, Cypress creates the required folders and configuration files automatically when you complete the setup flow.

Add Cypress Scripts to package.json

Instead of typing the full command every time, add scripts to package.json:

{

  "scripts": {

    "cypress:open": "cypress open",

    "cypress:run": "cypress run"

  }

}

Then run Cypress using:

npm run cypress:open

or:

npm run cypress:run

Output –

npm-run-scripts

Once Cypress is installed and opened, the next step is to understand the files and folders it creates in the project.

Cypress Folders and Project Structure

After Cypress is installed and opened for the first time, it creates a basic project structure. This structure helps separate test files, reusable test data, support logic, and configuration.

A typical Cypress project looks like this:

Cypress Project Structure

Here is what each file and folder is used for:

File/FolderPurpose
cypress/e2e/Stores end-to-end test files. Test files usually end with .cy.js, .cy.ts, .spec.js, or .spec.ts.
cypress/fixtures/Stores reusable test data, usually in JSON files. This is useful for form inputs, mock API responses, and sample user data.
cypress/support/Stores shared setup logic, custom commands, and code that should run before tests.
cypress/support/commands.jsDefines reusable custom commands such as cy.login() or cy.fillCheckoutForm().
cypress/support/e2e.jsRuns before every end-to-end spec file and is commonly used to import custom commands or global setup code.
cypress.config.jsMain Cypress configuration file. It controls settings such as base URL, viewport size, retries, timeouts, screenshots, videos, and environment variables.
package.jsonStores project dependencies and scripts used to run Cypress commands.

As the test suite grows, teams usually organize tests by feature or user flow:

Cypress Project

This makes the suite easier to maintain because related tests stay grouped together. For example, all login-related tests can live inside an auth folder, while payment and cart tests can live inside a checkout folder.

The default Cypress structure is simple, but it can be extended as the project grows. Larger teams often add folders for page objects, test utilities, API helpers, custom commands, and shared constants.

cypress.config.js/ts Explained

The cypress.config.js or cypress.config.ts file is the control center of a Cypress project. It defines how Cypress should run tests, which environment values to use, how long Cypress should wait before timing out, whether screenshots and videos should be captured, and how results should be reported.

A basic Cypress config file looks like this:

const { defineConfig } = require('cypress') 

module.exports = defineConfig({ 

viewportWidth: 1280, 

viewportHeight: 720, 

defaultCommandTimeout: 10000, 

retries: { 

runMode: 2, 

openMode: 0 

}, 

screenshotsFolder: 'cypress/screenshots', 

videosFolder: 'cypress/videos', 

video: true, 

screenshotOnRunFailure: true, 

reporter: 'spec',

env: { 

apiUrl: 'https://api.example.com', 

userRole: 'admin' 

}, 

e2e: { 

baseUrl: 'https://example.com', 

setupNodeEvents(on, config) { 

// configure plugins, tasks, or event listeners here return config 

} 

} 

})

Output –

cypress config file

Here is what the main settings mean:

Config OptionWhat it Does
baseUrlSets the default application URL, so tests can use cy.visit(‘/login’) instead of the full URL.
viewportWidth / viewportHeightDefines the browser screen size used during test execution.
retriesRe-runs failed tests before marking them as failed. Useful in CI where failures may happen due to temporary environment issues.
defaultCommandTimeoutControls how long Cypress waits for commands such as cy.get() before failing.
envStores environment-specific values such as API URLs, feature flags, usernames, or test settings.
e2eContains configuration specific to end-to-end tests.
setupNodeEventsLets you define Node-side event listeners, plugins, custom tasks, and reporting logic.
videoEnables or disables video recording during cypress run.
screenshotOnRunFailureCaptures a screenshot automatically when a test fails.
reporterDefines how test results should be displayed or exported.

The baseUrl option is especially useful because it keeps tests clean and environment-independent:

cy.visit('/login')

Instead of:

cy.visit('https://example.com/login')

For CI/CD pipelines, teams usually override values using environment variables. For example, the same test suite can run against staging, production, or preview environments without changing the test code:

CYPRESS_BASE_URL=https://staging.example.com npx cypress run

Output –

env override run

You can also read environment values inside tests:

cy.request(Cypress.env('apiUrl') + '/users')

A well-maintained Cypress config keeps test behavior consistent across local machines and CI environments. It also prevents repeated settings from being scattered across individual test files.

Cypress Test Runner Explained

The Cypress Test Runner is where you write, run, watch, and debug tests interactively. Instead of only showing a pass or fail result in the terminal, Cypress gives you a browser-based view of every test step as it runs.

When you open Cypress using:

npx cypress open

Output –

npx cypress open

Cypress launches the Test Runner. From there, you can choose a testing type, select a browser, pick a spec file, and watch the test execute inside the browser.

Command Log and Time-Travel Snapshots

The command log is one of the most useful parts of the Cypress Test Runner. Every command in your test appears in order:

cy.visit('/login')

cy.get('[data-cy="email"]').type('user@example.com')

cy.get('[data-cy="password"]').type('password123')

cy.get('[data-cy="login-button"]').click()

cy.url().should('include', '/dashboard')

In the runner, each of these commands is logged step by step. You can click on a command and inspect what the application looked like at that moment. This is called time-travel debugging.

For example, if the login button click fails, you can click that command in the runner and check whether the button was visible, disabled, covered by another element, or missing from the page.

This makes Cypress easier to debug than tools that only provide terminal logs or stack traces after the test fails.

Retryability in the Test Runner

Cypress automatically retries many commands and assertions before failing the test. For example:

cy.get('[data-cy="success-message"]').should('be.visible')

Retry behaviour –

retry visibility

Cypress does not check this only once. It keeps retrying until the success message becomes visible or the command times out.

This retry behavior helps with modern web applications where elements often appear after API calls, route changes, animations, or JavaScript updates. Instead of adding fixed waits like:

cy.wait(5000)

Output –

intercept wait

Cypress waits for the expected condition and moves forward as soon as it is met.

Mocha Structure in Cypress

Cypress tests use Mocha-style syntax. Test files are usually organized using describe(), it(), and hooks such as beforeEach() and afterEach().

Example:

describe('Login flow', () => {

  beforeEach(() => {

    cy.visit('/login')

  })


  it('logs in with valid credentials', () => {

    cy.get('[data-cy="email"]').type('user@example.com')

    cy.get('[data-cy="password"]').type('password123')

    cy.get('[data-cy="login-button"]').click()


    cy.url().should('include', '/dashboard')

  })

})

Output –

login-flow-passing

Here is what each part does:

PartPurpose
describe()Groups related tests together
it()Defines a single test case
beforeEach()Runs setup before each test
afterEach()Runs cleanup after each test
before()Runs once before all tests in the block
after()Runs once after all tests in the block

Hooks help avoid repeated setup code. For example, if every login test starts from the login page, cy.visit(‘/login’) can be placed inside beforeEach() instead of being repeated in every test.

Test Isolation

Cypress is designed to keep tests independent. Test isolation means one test should not depend on the result or state of another test.

For example, this is a bad pattern:

it('logs in', () => {

  cy.login()

})



it('checks dashboard', () => {

  cy.get('[data-cy="dashboard"]').should('be.visible')

})

Output –

bad-isolation-failing

The second test depends on the first test already logging in. If the tests run separately or in a different order, the dashboard test may fail.

A better approach is:

beforeEach(() => {

  cy.login()

})



it('checks dashboard', () => {

  cy.get('[data-cy="dashboard"]').should('be.visible')

})

Output –

good-isolation-passing

This way, each test starts with the state it needs.

Screenshots and Videos

Cypress can capture screenshots and videos during test runs. These are especially useful in CI, where you cannot watch the test run interactively.

When a test fails during cypress run, Cypress can automatically capture a screenshot of the failed state. Video recording can also be enabled to see the full test execution.

These settings are controlled in cypress.config.js or cypress.config.ts:

module.exports = defineConfig({

  video: true,

  screenshotOnRunFailure: true

})

Screenshots and videos help you understand whether a failure was caused by a missing element, slow API response, layout issue, wrong selector, or unexpected page state.

Writing your First Test Case for Cypress Automation

The cypressdemo folder contains

  1. node_modules folder
  2. cypress folder
  3. cypress.json file
  4. package.json file
  5. package-lock.json file.

To create your tests, navigate to cypress/integration and create a fresh new folder (eg: demo).

Inside the demo folder, create the test file (ex: firsttest.js) using the code below:

//firsttest.js



describe('My First Test', () => {



    it('Launch Browser and Navigate', () => {



        cy.visit('https://www.browserstack.com/');



        cy.title().should('eq', 'Most Reliable App & Cross Browser Testing Platform | BrowserStack')



    })



  })

How to Run The Test Cases Locally

You can use Cypress commands in your terminal to execute the test cases locally. Cypress offers the provision to run tests in headed and headless modes.

Headed Mode

When it’s the headed mode, you can perform your tests in a visible browser window and see the execution process in real time.

To run your Cypress tests in headed mode, you can use the cypress open command to launch the Cypress Test Runner to select and run different tests in an interactive manner.

npx cypress open

Headless Mode

In headless mode, you perform tests in the background without having to open a visible browser. The headless execution is usually done in Continuous Integration environments. You can use the cypress run command to run all tests without opening a browser window.

npx cypress run

Output –

headless run results

Best Practices for Cypress Automation

  1. Create Independent Tests: Isolate the tests as much as possible. Don’t create tests dependent on each other.
  2. Authenticate Applications Programmatically: Authentication or Logging into your application should be handled programmatically (Example: Using API calls), reducing testing dependency.
  3. data-* Attributes: Adding attributes to UI elements such as data-test, data-testid, or data-cy increases application testability and reduces dependency on selectors making the test stable.
  4. Utilize Cypress Architecture for easy and stable tests.
//Avoid



const myBtn = cy.contains('button', 'Click Me')



myBtn.click()



//Use



cy.contains('button', 'Click Me').as('myBtn')


cy.get('@myBtn').click()
  1. Avoid Using the after and afterEach Hooks: There are chances that code inside the after and afterEach hooks may not execute as expected. In that case, Cypress may halt test execution and not execute remaining tests.
  2. Avoid using cy.wait(): cy.wait() may slow down your test execution. Instead, rely on default wait mechanisms.
cy.intercept(...).as('req')



...



cy.wait('@req')

7. Do Not Start Your Web Server Using cy.task() or cy.exec(): Before test execution starts ensure your application is up and running.
8. cypress.json is a cypress configuration file: Set the correct baseURL inside the configuration file and/or create multiple configuration files as per the environment. You can also use the environment variable to execute tests as per the required environment.

Tips for Efficient Cypress Automation Testing

  1. Use cypress.json file to configure baseUrl, browser type, etc.
  2. Cypress captures videos and screenshots for tests which can be disabled using cypress.json entries.
"video": false,

"screenshotOnRunFailure":false
  1. Create a Shortcut command for executing your tests. Add entries to package.json scripts.

Example:

"scripts": {"test" : "cypress run --spec './cypress/integration/demo/firsttest.js'"},
  1. You can override the default timeout settings as per requirements.
  2. In the Cypress Test Runner UI, navigate to Settings, to view all configurations.

Conclusion

Understanding Cypress can significantly enhance your automation testing. Thanks to its user-friendly interface and robust features, you can write, run, and debug tests seamlessly to validate the functioning of your apps.

Tags
Automation Testing Cypress
Sujay Sawant
Sujay Sawant

Lead - Solution Engineer

Sujay Sawant has spent 11+ years across software engineering, QA, and customer engineering, giving him a well-rounded view of how systems are built and tested. He focuses on creating solutions that are reliable, easy to understand, and ready for real-world use.

Automation Tests on Real Devices & Browsers
Seamlessly Run Automation Tests on 3500+ real Devices & Browsers