TL;DR

  • Cypress runs inside the browser, making tests fast and reliable without WebDriver
  • Install with npm install cypress --save-dev, then run npx cypress open
  • Use data-* attributes for selectors — they survive UI changes

Best for: Beginners learning test automation, teams testing JavaScript web apps Skip if: You need mobile app testing or Safari support out of the box

I remember my first week trying to automate browser tests with Selenium. Configuration files, driver downloads, obscure error messages about session IDs. I spent more time debugging my test setup than writing actual tests. When I discovered Cypress, everything changed. I had a working test in 15 minutes.

Cypress has become the go-to choice for testing modern web applications. It runs directly inside the browser, provides instant feedback, and requires minimal configuration. This guide walks you through everything from installation to running tests in CI/CD pipelines.

Why Cypress?

Before diving into code, let’s understand what makes Cypress different from traditional testing tools.

Cypress vs Traditional Tools:

FeatureCypressSelenium/WebDriver
ArchitectureRuns inside browserExternal driver process
Setup timeMinutesHours (drivers, configs)
DebuggingTime-travel, snapshotsScreenshots, logs
SpeedFast (no network hops)Slower (HTTP commands)
FlakinessLow (automatic waits)Higher (manual waits)

Cypress excels at testing single-page applications built with React, Vue, Angular, or similar frameworks. It provides real-time reloading, automatic waiting, and a debugging experience that feels like using browser DevTools.

If you’re building a comprehensive testing strategy, understanding the test automation pyramid helps you decide where Cypress fits alongside unit tests and API tests.

Installing Cypress

Prerequisites

You need Node.js installed on your machine. Any version from 18.x onwards works well.

# Check Node.js version
node --version

# Should output v18.x.x or higher

Installation Steps

1. Initialize a project (if you don’t have one):

mkdir my-cypress-tests
cd my-cypress-tests
npm init -y

2. Install Cypress:

npm install cypress --save-dev

This downloads Cypress and its Electron browser. The first installation takes a minute or two.

3. Open Cypress:

npx cypress open

This launches the Cypress Test Runner — a visual interface for running tests.

First Launch Experience

When you open Cypress for the first time, it creates a folder structure:

cypress/
├── e2e/              # Your test files go here
├── fixtures/         # Test data (JSON files)
├── support/          # Custom commands and setup
│   ├── commands.js   # Custom commands
│   └── e2e.js        # Runs before each test file
└── downloads/        # Downloaded files during tests

Cypress also creates cypress.config.js in your project root — this is your main configuration file.

Writing Your First Test

Let’s write a test that visits a page and verifies its content. Create a file cypress/e2e/first-test.cy.js:

describe('My First Test', () => {
  it('visits the kitchen sink', () => {
    cy.visit('https://example.cypress.io')
    cy.contains('type').click()
    cy.url().should('include', '/commands/actions')
    cy.get('.action-email')
      .type('test@example.com')
      .should('have.value', 'test@example.com')
  })
})

Breaking it down:

  • describe() groups related tests
  • it() defines a single test case
  • cy.visit() navigates to a URL
  • cy.contains() finds element by text content
  • cy.get() finds element by CSS selector
  • .type() enters text into an input
  • .should() makes an assertion

Run this test by clicking on first-test.cy.js in the Test Runner.

Understanding Test Structure

Every Cypress test follows a pattern:

describe('Feature Name', () => {
  beforeEach(() => {
    // Runs before each test
    cy.visit('/login')
  })

  it('does something specific', () => {
    // Arrange: set up test conditions
    // Act: perform the action
    // Assert: verify the result
  })

  it('handles another scenario', () => {
    // Another test case
  })
})

The beforeEach hook runs before every test in the describe block. Use it for common setup like logging in or navigating to a page.

Selectors: Finding Elements

Finding the right elements is crucial for reliable tests. Cypress supports several selector strategies.

Selector Priority (Best to Worst)

1. Data attributes (recommended):

cy.get('[data-test="submit-button"]')
cy.get('[data-cy="login-form"]')
cy.get('[data-testid="user-email"]')

Data attributes exist specifically for testing. They don’t change when styling changes.

2. ID selectors:

cy.get('#username')

IDs are stable but not always available. Don’t add IDs just for testing — use data attributes instead.

3. Text content:

cy.contains('Submit')
cy.contains('button', 'Submit')  // More specific

Good for buttons and links. Breaks if text changes or gets translated.

4. CSS selectors (avoid if possible):

cy.get('.btn-primary')
cy.get('form input[type="email"]')

CSS classes change frequently. Complex selectors are fragile.

Adding Test Attributes to Your App

Work with your development team to add test attributes:

<!-- Before -->
<button class="btn btn-primary">Sign Up</button>

<!-- After -->
<button class="btn btn-primary" data-test="signup-button">Sign Up</button>

The data-test attribute survives refactoring, theming changes, and CSS updates.

Cypress Testing Library

For better selectors based on accessibility, install Testing Library:

npm install @testing-library/cypress --save-dev

Add to cypress/support/commands.js:

import '@testing-library/cypress/add-commands'

Now you can use accessible selectors:

cy.findByRole('button', { name: 'Submit' })
cy.findByLabelText('Email Address')
cy.findByPlaceholderText('Enter your email')

These selectors match how users interact with your app.

Assertions and Expectations

Cypress uses Chai assertions. The most common patterns:

Should Assertions

// Visibility
cy.get('[data-test="header"]').should('be.visible')
cy.get('[data-test="modal"]').should('not.exist')

// Content
cy.get('[data-test="title"]').should('have.text', 'Welcome')
cy.get('[data-test="title"]').should('contain', 'Welcome')

// Attributes
cy.get('input').should('have.value', 'test@example.com')
cy.get('a').should('have.attr', 'href', '/dashboard')

// State
cy.get('button').should('be.disabled')
cy.get('input').should('be.enabled')
cy.get('checkbox').should('be.checked')

// Count
cy.get('[data-test="list-item"]').should('have.length', 5)
cy.get('[data-test="list-item"]').should('have.length.gt', 3)

Chaining Assertions

cy.get('[data-test="user-card"]')
  .should('be.visible')
  .and('contain', 'John Doe')
  .and('have.class', 'active')

Expect for Complex Checks

cy.get('[data-test="product-list"]').then(($list) => {
  const itemCount = $list.find('.product').length
  expect(itemCount).to.be.greaterThan(0)
  expect(itemCount).to.be.lessThan(100)
})

Interacting with Elements

Common Actions

// Clicking
cy.get('button').click()
cy.get('button').dblclick()
cy.get('button').rightclick()

// Typing
cy.get('input').type('Hello World')
cy.get('input').type('Hello{enter}')  // Press Enter
cy.get('input').clear().type('New text')

// Selecting
cy.get('select').select('Option 1')
cy.get('select').select(['Option 1', 'Option 2'])  // Multi-select

// Checkboxes and Radios
cy.get('[type="checkbox"]').check()
cy.get('[type="checkbox"]').uncheck()
cy.get('[type="radio"]').check()

// File Upload
cy.get('input[type="file"]').selectFile('cypress/fixtures/image.png')

// Scrolling
cy.get('[data-test="footer"]').scrollIntoView()

Handling Forms

A typical form test:

describe('Contact Form', () => {
  beforeEach(() => {
    cy.visit('/contact')
  })

  it('submits successfully with valid data', () => {
    cy.get('[data-test="name"]').type('Jane Doe')
    cy.get('[data-test="email"]').type('jane@example.com')
    cy.get('[data-test="message"]').type('Hello from Cypress!')

    cy.get('[data-test="submit"]').click()

    cy.get('[data-test="success-message"]')
      .should('be.visible')
      .and('contain', 'Thank you')
  })

  it('shows error for invalid email', () => {
    cy.get('[data-test="email"]').type('invalid-email')
    cy.get('[data-test="submit"]').click()

    cy.get('[data-test="email-error"]')
      .should('be.visible')
      .and('contain', 'valid email')
  })
})

Working with APIs

Cypress can intercept and mock API calls. This makes tests faster and more reliable.

Intercepting Requests

cy.intercept('GET', '/api/users').as('getUsers')

cy.visit('/users')
cy.wait('@getUsers')

cy.get('[data-test="user-list"]').should('be.visible')

The cy.wait('@getUsers') pauses until the API call completes. No more cy.wait(5000) hacks.

Mocking API Responses

cy.intercept('GET', '/api/products', {
  statusCode: 200,
  body: [
    { id: 1, name: 'Product A', price: 29.99 },
    { id: 2, name: 'Product B', price: 39.99 }
  ]
}).as('getProducts')

cy.visit('/products')
cy.wait('@getProducts')

cy.get('[data-test="product"]').should('have.length', 2)

Mocking lets you test edge cases without backend changes.

Testing Error States

cy.intercept('POST', '/api/checkout', {
  statusCode: 500,
  body: { error: 'Payment failed' }
}).as('checkoutError')

cy.get('[data-test="checkout-button"]').click()
cy.wait('@checkoutError')

cy.get('[data-test="error-message"]')
  .should('be.visible')
  .and('contain', 'Payment failed')

For deep coverage of network stubbing techniques, check Cypress Deep Dive: Architecture, Debugging, and Network Stubbing.

Test Configuration

cypress.config.js

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    defaultCommandTimeout: 10000,
    video: false,
    screenshotOnRunFailure: true,

    setupNodeEvents(on, config) {
      // Node event listeners here
    }
  }
})

Common settings:

SettingDescriptionDefault
baseUrlPrepended to cy.visit() URLsnone
viewportWidth/HeightBrowser dimensions1000 × 660
defaultCommandTimeoutHow long to retry commands4000ms
videoRecord videos of test runstrue
retriesAuto-retry failed tests0

Environment Variables

// cypress.config.js
module.exports = defineConfig({
  e2e: {
    env: {
      apiUrl: 'http://localhost:4000',
      adminUser: 'admin@test.com'
    }
  }
})

Access in tests:

cy.visit(Cypress.env('apiUrl') + '/login')
cy.get('input').type(Cypress.env('adminUser'))

Or pass via command line:

npx cypress run --env apiUrl=http://staging.example.com

Custom Commands

Create reusable commands in cypress/support/commands.js:

Cypress.Commands.add('login', (email, password) => {
  cy.visit('/login')
  cy.get('[data-test="email"]').type(email)
  cy.get('[data-test="password"]').type(password)
  cy.get('[data-test="submit"]').click()
  cy.url().should('include', '/dashboard')
})

Cypress.Commands.add('logout', () => {
  cy.get('[data-test="user-menu"]').click()
  cy.get('[data-test="logout"]').click()
})

Use in tests:

describe('Dashboard', () => {
  beforeEach(() => {
    cy.login('user@example.com', 'password123')
  })

  it('shows user profile', () => {
    cy.get('[data-test="profile"]').should('be.visible')
  })
})

Programmatic Login

For faster tests, skip the UI login:

Cypress.Commands.add('loginByApi', (email, password) => {
  cy.request({
    method: 'POST',
    url: '/api/auth/login',
    body: { email, password }
  }).then((response) => {
    window.localStorage.setItem('authToken', response.body.token)
  })
})

This is much faster than filling out forms repeatedly.

Organizing Tests

Folder Structure

cypress/
├── e2e/
│   ├── auth/
│   │   ├── login.cy.js
│   │   └── registration.cy.js
│   ├── products/
│   │   ├── listing.cy.js
│   │   └── checkout.cy.js
│   └── smoke/
│       └── critical-paths.cy.js
├── fixtures/
│   ├── users.json
│   └── products.json
└── support/
    ├── commands.js
    └── e2e.js

Using Fixtures

Store test data in cypress/fixtures/:

// cypress/fixtures/users.json
{
  "admin": {
    "email": "admin@example.com",
    "password": "admin123"
  },
  "regular": {
    "email": "user@example.com",
    "password": "user123"
  }
}

Load in tests:

cy.fixture('users').then((users) => {
  cy.login(users.admin.email, users.admin.password)
})

// Or use directly in intercepts
cy.intercept('GET', '/api/users', { fixture: 'users.json' })

Running Tests in CI/CD

Command Line Execution

# Run all tests
npx cypress run

# Run specific test file
npx cypress run --spec "cypress/e2e/auth/*.cy.js"

# Run in specific browser
npx cypress run --browser chrome

# Run with environment variables
npx cypress run --env apiUrl=http://staging.example.com

GitHub Actions Integration

Create .github/workflows/cypress.yml:

name: Cypress Tests

on: [push, pull_request]

jobs:
  cypress:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Start application
        run: npm start &

      - name: Wait for app
        run: npx wait-on http://localhost:3000

      - name: Run Cypress tests
        uses: cypress-io/github-action@v6
        with:
          wait-on: 'http://localhost:3000'
          browser: chrome

      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: cypress-screenshots
          path: cypress/screenshots

For more CI/CD patterns, see GitHub Actions for QA Automation.

Parallel Execution

For large test suites, run tests in parallel:

jobs:
  cypress:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        containers: [1, 2, 3]

    steps:
      - uses: cypress-io/github-action@v6
        with:
          record: true
          parallel: true
          group: 'UI Tests'
        env:
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

This requires Cypress Cloud (free tier available).

Debugging Failed Tests

Time Travel

Click on any command in the Test Runner to see the DOM state at that moment. This is incredibly useful for understanding why a selector failed.

Screenshots and Videos

// Take a screenshot manually
cy.screenshot('before-submit')

// Screenshots are automatic on failure
// Videos record the entire test run

Configure in cypress.config.js:

module.exports = defineConfig({
  e2e: {
    screenshotOnRunFailure: true,
    video: true,
    videosFolder: 'cypress/videos',
    screenshotsFolder: 'cypress/screenshots'
  }
})

Debug Command

cy.get('[data-test="menu"]').debug()

This pauses execution and logs the element to the console.

Console Logging

cy.get('[data-test="items"]').then(($items) => {
  console.log('Item count:', $items.length)
  console.log('First item:', $items.first().text())
})

Open browser DevTools to see the output.

Best Practices

Do

  • Use data-test attributes for selectors
  • Wait for API calls instead of arbitrary timeouts
  • Keep tests independent (each can run alone)
  • Use beforeEach for common setup
  • Test one thing per test case
  • Mock external services

Don’t

  • Use cy.wait(5000) — wait for specific events
  • Share state between tests
  • Test implementation details (CSS classes, HTML structure)
  • Over-mock — some tests should hit real APIs
  • Write huge tests — keep them focused

Handling Flaky Tests

// Bad - timing-dependent
cy.wait(3000)
cy.get('[data-test="results"]').should('exist')

// Good - wait for specific condition
cy.intercept('GET', '/api/search*').as('search')
cy.get('[data-test="search"]').type('query')
cy.wait('@search')
cy.get('[data-test="results"]').should('exist')

What’s Next?

Once you’re comfortable with the basics, explore these advanced topics:

  • Component testing: Test React/Vue components in isolation
  • Visual regression: Catch unexpected UI changes
  • Performance monitoring: Track load times
  • Accessibility testing: Ensure your app works for everyone

For advanced Cypress patterns including architecture deep dives and network stubbing mastery, read Cypress Deep Dive. If you’re evaluating alternatives, Playwright Comprehensive Guide covers Microsoft’s competing framework.

FAQ

Is Cypress free to use?

Yes, Cypress is open-source and completely free for local development and CI/CD. Cypress Cloud (formerly Dashboard) offers a free tier with limited test recordings. Paid tiers provide more parallel runs, longer history retention, and advanced analytics. Most teams start free and upgrade as test suites grow.

Do I need to know JavaScript to use Cypress?

Basic JavaScript knowledge helps, but you don’t need to be an expert. Cypress syntax is designed to be readable — cy.get('button').click() is understandable even without JavaScript experience. Start with simple tests and learn JavaScript patterns (promises, arrow functions, destructuring) as you need them. Many QA engineers learn JavaScript through Cypress.

Can Cypress test any website?

Cypress tests web applications that run in a browser. It works with any frontend framework: React, Vue, Angular, vanilla JavaScript, or server-rendered pages. However, Cypress cannot test native mobile apps (iOS/Android), desktop applications, or Electron apps in production builds. For mobile testing, you need tools like Appium. Cypress also has limitations with cross-origin iframes and multiple browser tabs.

How long does it take to learn Cypress?

You can write and run your first test within an hour of installation. Basic proficiency — writing reliable tests, using selectors properly, handling forms — takes 2-4 weeks of regular practice. Advanced skills like custom commands, API mocking, and CI/CD integration take longer. Most engineers feel confident after automating their first real feature end-to-end.

See Also

Official Resources