TL;DR
- Cypress runs inside the browser, making tests fast and reliable without WebDriver
- Install with
npm install cypress --save-dev, then runnpx cypress open- Use
data-*attributes for selectors — they survive UI changesBest 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:
| Feature | Cypress | Selenium/WebDriver |
|---|---|---|
| Architecture | Runs inside browser | External driver process |
| Setup time | Minutes | Hours (drivers, configs) |
| Debugging | Time-travel, snapshots | Screenshots, logs |
| Speed | Fast (no network hops) | Slower (HTTP commands) |
| Flakiness | Low (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 testsit()defines a single test casecy.visit()navigates to a URLcy.contains()finds element by text contentcy.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:
| Setting | Description | Default |
|---|---|---|
baseUrl | Prepended to cy.visit() URLs | none |
viewportWidth/Height | Browser dimensions | 1000 × 660 |
defaultCommandTimeout | How long to retry commands | 4000ms |
video | Record videos of test runs | true |
retries | Auto-retry failed tests | 0 |
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-testattributes for selectors - Wait for API calls instead of arbitrary timeouts
- Keep tests independent (each can run alone)
- Use
beforeEachfor 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
- Cypress Deep Dive: Architecture and Network Stubbing - Advanced techniques after you master basics
- Playwright Comprehensive Guide - Compare with Microsoft’s alternative
- Test Automation Pyramid Strategy - Where Cypress fits in your testing strategy
- GitHub Actions for QA Automation - CI/CD integration patterns
- Continuous Testing in DevOps - Integrating automated tests in your pipeline
