What is Cypress?
Cypress is a next-generation front-end testing tool built for the modern web. It enables developers to write end-to-end tests, integration tests, and unit tests with a focus on developer experience and debugging capabilities. Unlike Selenium, Cypress runs directly in the browser alongside your application, providing real-time reloads, time travel debugging, and automatic waiting.
Why Cypress Matters:
- Fast, reliable, and flake-resistant tests
- Real-time browser preview and debugging
- Automatic waiting eliminates test flakiness
- Excellent developer experience with detailed error messages
- Built-in screenshots and video recording
- No need for explicit waits or sleep statements
Core Concepts & Principles
Cypress Architecture
- Test Runner: Interactive GUI for writing and debugging tests
- Dashboard: Cloud-based service for CI/CD integration and analytics
- Browser Control: Direct browser automation without WebDriver
- Network Stubbing: Intercept and mock network requests
- Time Travel: Debug tests by stepping through commands
Key Principles
- Tests run in the same event loop as your application
- Cypress automatically waits for elements and assertions
- Tests are written in JavaScript/TypeScript
- Each test runs in isolation with a clean slate
- Cypress can test anything that runs in a browser
Installation & Setup
Quick Start Installation
# Install Cypress via npm
npm install cypress --save-dev
# Install Cypress via yarn
yarn add cypress --dev
# Open Cypress Test Runner
npx cypress open
# Run Cypress tests headlessly
npx cypress run
Project Structure
cypress/
├── e2e/ # End-to-end test files
├── fixtures/ # Test data files
├── support/ # Custom commands and utilities
└── cypress.config.js # Configuration file
Basic Configuration (cypress.config.js)
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
defaultCommandTimeout: 10000,
requestTimeout: 10000,
responseTimeout: 10000,
},
})
Essential Cypress Commands
Navigation & Page Interaction
| Command | Purpose | Example |
|---|---|---|
cy.visit() | Navigate to URL | cy.visit('/login') |
cy.go() | Browser navigation | cy.go('back'), cy.go('forward') |
cy.reload() | Refresh page | cy.reload() |
cy.title() | Get page title | cy.title().should('eq', 'My App') |
cy.url() | Get current URL | cy.url().should('include', '/dashboard') |
Element Selection & Interaction
| Command | Purpose | Example |
|---|---|---|
cy.get() | Select elements | cy.get('[data-testid="submit-btn"]') |
cy.contains() | Find by text content | cy.contains('Login').click() |
cy.click() | Click element | cy.get('#button').click() |
cy.type() | Type text | cy.get('#email').type('user@example.com') |
cy.clear() | Clear input field | cy.get('#search').clear() |
cy.check() | Check checkbox/radio | cy.get('#terms').check() |
cy.uncheck() | Uncheck checkbox | cy.get('#newsletter').uncheck() |
cy.select() | Select from dropdown | cy.get('#country').select('Canada') |
Assertions & Validation
| Command | Purpose | Example |
|---|---|---|
.should() | Make assertions | cy.get('#error').should('be.visible') |
.and() | Chain assertions | cy.get('#input').should('have.value', 'test').and('be.visible') |
.expect() | BDD assertions | expect(result).to.equal('success') |
Common Assertion Patterns
// Visibility assertions
cy.get('#element').should('be.visible')
cy.get('#element').should('not.exist')
cy.get('#element').should('be.hidden')
// Text content assertions
cy.get('#title').should('contain.text', 'Welcome')
cy.get('#message').should('have.text', 'Success!')
// Attribute assertions
cy.get('#link').should('have.attr', 'href', '/home')
cy.get('#input').should('have.class', 'active')
cy.get('#button').should('be.disabled')
// Length assertions
cy.get('.list-item').should('have.length', 5)
cy.get('.error').should('have.length.greaterThan', 0)
Advanced Testing Techniques
Network Requests & API Testing
Intercepting Network Requests
// Intercept and mock API calls
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers')
cy.visit('/users')
cy.wait('@getUsers')
// Intercept with dynamic responses
cy.intercept('POST', '/api/login', (req) => {
if (req.body.email === 'admin@test.com') {
req.reply({ statusCode: 200, body: { token: 'abc123' } })
} else {
req.reply({ statusCode: 401, body: { error: 'Invalid credentials' } })
}
}).as('login')
Testing API Endpoints
// Direct API testing
cy.request({
method: 'POST',
url: '/api/users',
body: { name: 'John Doe', email: 'john@example.com' }
}).then((response) => {
expect(response.status).to.eq(201)
expect(response.body).to.have.property('id')
})
File Handling
| Command | Purpose | Example |
|---|---|---|
cy.fixture() | Load test data | cy.fixture('users.json').then((users) => {}) |
cy.readFile() | Read file content | cy.readFile('cypress/downloads/report.pdf') |
cy.writeFile() | Write to file | cy.writeFile('path/to/file.txt', 'content') |
cy.selectFile() | Upload files | cy.get('#file-input').selectFile('path/to/image.jpg') |
Custom Commands
Create reusable commands in cypress/support/commands.js:
// Login command
Cypress.Commands.add('login', (email, password) => {
cy.visit('/login')
cy.get('#email').type(email)
cy.get('#password').type(password)
cy.get('#login-btn').click()
cy.url().should('include', '/dashboard')
})
// Database seeding command
Cypress.Commands.add('seedDatabase', (fixture) => {
cy.task('db:seed', fixture)
})
// Usage in tests
cy.login('user@example.com', 'password123')
Testing Strategies & Best Practices
Test Organization Patterns
Page Object Model
// pages/LoginPage.js
class LoginPage {
visit() {
cy.visit('/login')
}
fillEmail(email) {
cy.get('#email').type(email)
return this
}
fillPassword(password) {
cy.get('#password').type(password)
return this
}
submit() {
cy.get('#login-btn').click()
return this
}
}
export default new LoginPage()
// Usage in tests
import LoginPage from '../pages/LoginPage'
it('should login successfully', () => {
LoginPage.visit()
.fillEmail('user@test.com')
.fillPassword('password')
.submit()
})
Data-Driven Testing
// Using fixtures for test data
describe('User Registration', () => {
beforeEach(() => {
cy.fixture('users').as('userData')
})
it('should register multiple users', function() {
this.userData.forEach((user) => {
cy.visit('/register')
cy.get('#name').type(user.name)
cy.get('#email').type(user.email)
cy.get('#password').type(user.password)
cy.get('#register-btn').click()
})
})
})
Element Selection Best Practices
Selector Priority (Recommended Order)
| Priority | Selector Type | Example | Why Use |
|---|---|---|---|
| 1 | data-testid | [data-testid="submit-button"] | Test-specific, stable |
| 2 | data-cy | [data-cy="user-menu"] | Cypress-specific |
| 3 | ID | #login-form | Unique identifier |
| 4 | Class | .btn-primary | Semantic meaning |
| 5 | Text content | cy.contains('Save') | User-facing text |
| 6 | XPath/CSS | input[type="email"] | Last resort |
Selector Examples
// ✅ Good selectors
cy.get('[data-testid="user-profile"]')
cy.get('[data-cy="navigation-menu"]')
cy.get('#search-input')
cy.contains('button', 'Submit')
// ❌ Avoid these selectors
cy.get('.css-1234abc') // Generated classes
cy.get('div > span:nth-child(3)') // Brittle structure
cy.get('button:contains("Submit")') // Non-standard syntax
Testing Scenarios & Examples
Form Testing
describe('Contact Form', () => {
it('should submit form with valid data', () => {
cy.visit('/contact')
cy.get('#name').type('John Doe')
cy.get('#email').type('john@example.com')
cy.get('#message').type('Hello, this is a test message.')
cy.get('#submit').click()
cy.get('.success-message')
.should('be.visible')
.and('contain', 'Message sent successfully')
})
it('should show validation errors', () => {
cy.visit('/contact')
cy.get('#submit').click()
cy.get('#name-error').should('contain', 'Name is required')
cy.get('#email-error').should('contain', 'Email is required')
})
})
Authentication Testing
describe('Authentication', () => {
it('should login with valid credentials', () => {
cy.visit('/login')
cy.get('#email').type('user@example.com')
cy.get('#password').type('password123')
cy.get('#login-btn').click()
cy.url().should('include', '/dashboard')
cy.get('[data-testid="user-menu"]').should('be.visible')
cy.getCookie('auth-token').should('exist')
})
it('should logout successfully', () => {
cy.login('user@example.com', 'password123') // Custom command
cy.get('[data-testid="logout-btn"]').click()
cy.url().should('include', '/login')
cy.getCookie('auth-token').should('not.exist')
})
})
E-commerce Testing
describe('Shopping Cart', () => {
it('should add items to cart', () => {
cy.visit('/products')
cy.get('[data-testid="product-1"] .add-to-cart').click()
cy.get('[data-testid="product-2"] .add-to-cart').click()
cy.get('[data-testid="cart-counter"]').should('contain', '2')
cy.get('[data-testid="cart-icon"]').click()
cy.get('.cart-items').should('have.length', 2)
cy.get('.cart-total').should('contain', '$39.98')
})
})
Common Challenges & Solutions
Challenge: Flaky Tests
Problem: Tests that randomly pass or fail
Solutions:
// ❌ Bad: Using fixed waits
cy.wait(5000)
// ✅ Good: Wait for specific conditions
cy.get('#loading').should('not.exist')
cy.get('#data-table').should('be.visible')
// ✅ Good: Increase command timeout for slow elements
cy.get('#slow-element', { timeout: 10000 }).should('be.visible')
// ✅ Good: Retry assertions
cy.get('#dynamic-content').should('contain', 'Updated', { timeout: 5000 })
Challenge: Handling Async Operations
Problem: Dealing with dynamic content and AJAX calls
Solutions:
// Wait for network requests
cy.intercept('GET', '/api/data').as('getData')
cy.get('#refresh-btn').click()
cy.wait('@getData')
// Wait for DOM changes
cy.get('#counter').should('contain', '0')
cy.get('#increment').click()
cy.get('#counter').should('contain', '1')
// Wait for element to appear
cy.get('#modal').should('be.visible')
Challenge: Testing Third-party Integrations
Problem: External dependencies in tests
Solutions:
// Mock external APIs
cy.intercept('GET', 'https://api.external.com/**', {
statusCode: 200,
body: { data: 'mocked response' }
})
// Skip tests conditionally
it('should work with external service', () => {
cy.task('checkExternalService').then((isAvailable) => {
if (!isAvailable) {
cy.skip('External service unavailable')
}
// Test logic here
})
})
CI/CD Integration
GitHub Actions Example
name: Cypress Tests
on: [push, pull_request]
jobs:
cypress-run:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Cypress run
uses: cypress-io/github-action@v5
with:
build: npm run build
start: npm start
wait-on: 'http://localhost:3000'
wait-on-timeout: 120
browser: chrome
record: true
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
Docker Configuration
FROM cypress/included:12.9.0
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx cypress verify
CMD ["npx", "cypress", "run"]
Performance Optimization
Speed Up Test Execution
| Technique | Implementation | Impact |
|---|---|---|
| Parallel Execution | cypress run --record --parallel | 50-70% faster |
| Skip Unnecessary Tests | it.skip() or describe.skip() | Variable |
| Optimize Selectors | Use data attributes | 10-20% faster |
| Mock Network Calls | cy.intercept() | 30-50% faster |
| Reduce Test Isolation | Share state between tests | 20-30% faster |
Configuration Optimizations
// cypress.config.js
module.exports = defineConfig({
e2e: {
// Reduce default timeouts for faster failures
defaultCommandTimeout: 4000,
requestTimeout: 5000,
responseTimeout: 5000,
// Disable video recording in CI
video: false,
// Only capture screenshots on failure
screenshotOnRunFailure: true,
// Optimize viewport for faster rendering
viewportWidth: 1000,
viewportHeight: 600,
},
})
Debugging & Troubleshooting
Debug Commands
| Command | Purpose | Usage |
|---|---|---|
cy.debug() | Pause test execution | cy.get('#element').debug() |
cy.pause() | Stop and allow interaction | cy.pause() |
cy.log() | Add custom log message | cy.log('Testing login flow') |
.then() | Access element/value | cy.get('#input').then(($el) => {}) |
Common Error Patterns
// Element not found
// ❌ cy.get('#nonexistent').click()
// ✅ cy.get('#element').should('exist').click()
// Timing issues
// ❌ cy.get('#button').click().get('#result')
// ✅ cy.get('#button').click().then(() => {
// cy.get('#result').should('be.visible')
// })
// Assertion failures
// ❌ cy.get('#element').should('have.text', 'exact text')
// ✅ cy.get('#element').should('contain.text', 'partial text')
Best Practices Checklist
Test Structure
- ✅ Use descriptive test names that explain the expected behavior
- ✅ Follow AAA pattern (Arrange, Act, Assert)
- ✅ Keep tests independent and isolated
- ✅ Use
beforeEach()for common setup - ✅ Clean up data after tests when necessary
Selectors & Elements
- ✅ Prefer
data-testidattributes for test-specific selectors - ✅ Avoid CSS selectors that depend on styling
- ✅ Use semantic selectors when possible
- ✅ Create page objects for complex applications
Assertions
- ✅ Use explicit assertions instead of implicit ones
- ✅ Assert on user-visible behavior
- ✅ Combine multiple assertions with
.and() - ✅ Wait for conditions rather than using fixed waits
Test Data
- ✅ Use fixtures for static test data
- ✅ Generate dynamic data when needed
- ✅ Clean up test data to avoid interference
- ✅ Use meaningful test data that reflects real scenarios
Essential Resources
Official Documentation
Community Resources
Video Tutorials
Tools & Plugins
- cypress-testing-library
- cypress-axe – Accessibility testing
- cypress-image-snapshot – Visual regression testing
- cypress-real-events – Real browser events
This cheat sheet covers Cypress 12+ features and is updated for modern web testing practices in 2025.
