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 Type | What it Tests | How Cypress Works |
|---|---|---|
| End-to-End Testing | A complete user flow across the application | Opens the app in a real browser and performs actions like a user |
| Component Testing | A single UI component in isolation | Mounts the component directly and tests its behavior without running the full app flow |
| API Testing | Backend responses and business logic exposed through APIs | Sends HTTP requests using cy.request() and validates the response |
| Visual Testing | Page layout, UI appearance, and visual regressions | Captures 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.
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:
- Open the login page.
- Find the email field.
- Type the email address.
- Find the password field.
- Type the password.
- Click the login button.
- 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:
| Step | What Happens |
|---|---|
| Test file is loaded | Cypress reads the test and identifies the commands |
| Commands are queued | Cypress adds commands like cy.visit(), cy.get(), and cy.click() to a queue |
| Commands run in order | Each command waits for the previous command to finish |
| Cypress retries automatically | Queries and assertions retry until they pass or time out |
| Results are recorded | The 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
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 –
Open Cypress for the First Time
After installation, open Cypress with:
npx cypress open
Output –
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 –
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:
Here is what each file and folder is used for:
| File/Folder | Purpose |
|---|---|
| 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.js | Defines reusable custom commands such as cy.login() or cy.fillCheckoutForm(). |
| cypress/support/e2e.js | Runs before every end-to-end spec file and is commonly used to import custom commands or global setup code. |
| cypress.config.js | Main Cypress configuration file. It controls settings such as base URL, viewport size, retries, timeouts, screenshots, videos, and environment variables. |
| package.json | Stores project dependencies and scripts used to run Cypress commands. |
As the test suite grows, teams usually organize tests by feature or user flow:
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 –
Here is what the main settings mean:
| Config Option | What it Does |
|---|---|
| baseUrl | Sets the default application URL, so tests can use cy.visit(‘/login’) instead of the full URL. |
| viewportWidth / viewportHeight | Defines the browser screen size used during test execution. |
| retries | Re-runs failed tests before marking them as failed. Useful in CI where failures may happen due to temporary environment issues. |
| defaultCommandTimeout | Controls how long Cypress waits for commands such as cy.get() before failing. |
| env | Stores environment-specific values such as API URLs, feature flags, usernames, or test settings. |
| e2e | Contains configuration specific to end-to-end tests. |
| setupNodeEvents | Lets you define Node-side event listeners, plugins, custom tasks, and reporting logic. |
| video | Enables or disables video recording during cypress run. |
| screenshotOnRunFailure | Captures a screenshot automatically when a test fails. |
| reporter | Defines 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 –
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 –
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 –
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 –
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 –
Here is what each part does:
| Part | Purpose |
|---|---|
| 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 –
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 –
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
- node_modules folder
- cypress folder
- cypress.json file
- package.json file
- 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 –
Best Practices for Cypress Automation
- Create Independent Tests: Isolate the tests as much as possible. Don’t create tests dependent on each other.
- Authenticate Applications Programmatically: Authentication or Logging into your application should be handled programmatically (Example: Using API calls), reducing testing dependency.
- 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.
- 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()- 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.
- 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
- Use cypress.json file to configure baseUrl, browser type, etc.
- Cypress captures videos and screenshots for tests which can be disabled using cypress.json entries.
"video": false, "screenshotOnRunFailure":false
- 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'"},- You can override the default timeout settings as per requirements.
- 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.














