Introduction to Cypress
Cypress is a next-generation front-end testing tool built for the modern web. It addresses the pain points developers and QA engineers face when testing modern applications, offering a more developer-friendly and reliable testing experience compared to traditional tools like Selenium.
Real-world analogy: If traditional E2E testing tools are like remotely controlling a car from the outside, Cypress is like being inside the car with direct access to the steering wheel, pedals, and dashboard. This gives you much more precise control and visibility into what's happening.
Cypress vs. Traditional Testing Tools
Cypress differs from traditional testing tools in several fundamental ways:
| Feature | Selenium-based Tools | Cypress |
|---|---|---|
| Architecture | External to browser, communicates via WebDriver | Runs directly in the browser, same run loop as app |
| Language Support | Multiple languages (Java, Python, Ruby, etc.) | JavaScript/TypeScript only |
| Browser Support | All major browsers | Chrome, Firefox, Edge, Electron (Safari via WebKit) |
| Waiting Strategy | Explicit waits, implicit waits, sleep | Automatic waiting, retries, timeouts |
| Debugging | Logs, screenshots | Time travel, real-time reloads, dev tools integration |
| Setup Complexity | Higher (drivers, configurations) | Lower (batteries included) |
| Network Control | Limited | Full network stubbing/mocking capabilities |
Key Advantages of Cypress
- Architecture: Runs inside the browser with direct access to the DOM, window, and application
- Automatic Waiting: Automatically waits for elements, animations, and network requests
- Real-time Reloading: Tests reload as you modify code
- Time Travel: Snapshots at each step let you see exactly what happened
- Network Control: Stub network requests and responses without server changes
- Debugging: Better visibility with browser dev tools integration
Real-world example: Shopify switched from a Selenium-based stack to Cypress for their storefront testing, reducing their test execution time by 60% and significantly increasing test reliability. The improved developer experience also led to better test coverage as developers became more willing to write and maintain tests.
Setting Up Cypress
Installation
Installing Cypress is straightforward using npm:
// Add Cypress as a development dependency
npm install cypress --save-dev
// Or with Yarn
yarn add cypress --dev
Opening Cypress
// Open Cypress Test Runner
npx cypress open
// Or add to package.json scripts
"scripts": {
"cypress:open": "cypress open",
"cypress:run": "cypress run"
}
Initial Setup
When you open Cypress for the first time, it creates a directory structure:
cypress/
├── fixtures/ # Test data
│ └── example.json
├── e2e/ # Test files
│ └── spec.cy.js
├── support/ # Support files
│ ├── commands.js # Custom commands
│ └── e2e.js # Global configuration
└── cypress.config.js # Cypress configuration
Configuration
The main configuration file is cypress.config.js:
// cypress.config.js
const { defineConfig } = require('cypress')
module.exports = defineConfig({
// Base URL for cy.visit() commands
baseUrl: 'http://localhost:3000',
// Viewport dimensions for tests
viewportWidth: 1280,
viewportHeight: 720,
e2e: {
// Folder where tests are located
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
// Setup function to run before tests
setupNodeEvents(on, config) {
// Plugins and event listeners
},
},
// Test retry settings
retries: {
runMode: 2,
openMode: 0
}
})
TypeScript Support
To use TypeScript with Cypress:
// Install TypeScript
npm install typescript --save-dev
// Create tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "node"]
},
"include": ["**/*.ts"]
}
Real-world example: Atlassian uses Cypress with TypeScript for testing their cloud products. The type safety helps catch errors early, and the improved autocomplete speeds up test development.
Writing Your First Cypress Test
Let's create a simple test that visits a website and interacts with it:
// cypress/e2e/first-test.cy.js
describe('My First Test', () => {
it('Visits the home page and clicks a link', () => {
// Visit the page
cy.visit('https://example.cypress.io')
// Find and click a link
cy.contains('type').click()
// Verify the URL changed
cy.url().should('include', '/commands/actions')
// Interact with a form element
cy.get('.action-email')
.type('hello@example.com')
.should('have.value', 'hello@example.com')
})
})
Test Structure
Cypress tests use a familiar structure:
describe- Groups related testsitortest- Individual test casescy.*- Cypress commands for interaction.should()- Assertions about the state
Basic Commands
Common Cypress commands include:
// Navigation
cy.visit('/about') // Visit a page
cy.go('back') // Navigate back
cy.reload() // Reload the page
// Finding elements
cy.get('.button') // Find by selector
cy.contains('Submit') // Find by text content
cy.find('input') // Find within a previous element
// Actions
cy.click() // Click an element
cy.type('Hello') // Type into an input
cy.check() // Check a checkbox
cy.select('Option 1') // Select from a dropdown
// Assertions
cy.should('exist') // Element exists
cy.should('have.text', 'Hi') // Element has text
cy.should('be.visible') // Element is visible
cy.should('have.class', 'active') // Element has class
Running Tests
Cypress offers two ways to run tests:
- Test Runner UI - Interactive, visual testing for development
- Headless Mode - Command-line execution for CI/CD
// Interactive mode
npx cypress open
// Headless mode
npx cypress run
// Run specific test file
npx cypress run --spec "cypress/e2e/login.cy.js"
Core Cypress Concepts
Automatic Waiting
Cypress automatically waits for elements to exist, be visible, and be actionable:
// Cypress will automatically wait up to 4 seconds for:
// 1. The button to exist in the DOM
// 2. The button to be visible (not hidden)
// 3. The button to be enabled (not disabled)
// 4. The button to not be covered by other elements
cy.get('button').click()
// No need for explicit waits like in Selenium:
// Selenium: driver.wait(until.elementIsVisible(button), 5000);
Assertions
Assertions in Cypress are chainable and retried until they pass or timeout:
// Built-in assertions with retry logic
cy.get('.user-name').should('have.text', 'John Doe')
// Multiple assertions in a chain
cy.get('form')
.should('be.visible')
.and('have.class', 'signup-form')
.and('not.have.class', 'submitted')
// Explicit assertions for complex logic
cy.get('.items').then(($items) => {
expect($items).to.have.length(5)
expect($items.eq(0)).to.contain('First Item')
})
Chaining and Subject Management
Cypress commands are chainable and pass their subjects:
// Commands form a chain, passing the subject
cy.get('form') // Subject: form element
.find('input[name="email"]') // Subject: email input inside form
.type('user@example.com') // Types into email input
.should('have.value', 'user@example.com') // Asserts on email input
// Breaking the chain with .then()
cy.get('form').then(($form) => {
// Inside .then(), we get a jQuery object, not a Cypress chain
const action = $form.attr('action')
expect(action).to.include('/submit')
// Start a new chain with cy.*
cy.get('input').type('Hello')
})
Aliases for Reuse
Use aliases to reference elements or values across your test:
// Save an element as an alias
cy.get('button.submit').as('submitButton')
// Later in your test
cy.get('@submitButton').click()
// Alias API responses
cy.intercept('GET', '/api/users').as('getUsers')
// Wait for the response
cy.wait('@getUsers').then((interception) => {
expect(interception.response.statusCode).to.equal(200)
})
Real-world example: Glitch, a collaborative coding platform, uses Cypress's automatic waiting and retry behavior to test their real-time collaborative features. This helps them handle the inherent timing complexity of testing collaborative editing.
Selectors and Element Interaction
Finding Elements
Cypress supports multiple ways to find elements:
// CSS selectors
cy.get('#login-button')
cy.get('.menu-item')
cy.get('header h1')
// Finding by content
cy.contains('Sign Up')
cy.contains('h2', 'Welcome') // Element type + content
// Combining selectors
cy.get('form').find('input[type="email"]')
// Getting elements by position
cy.get('li').first()
cy.get('li').last()
cy.get('li').eq(2) // Third item (zero-based index)
Best Practices for Selectors
Use selectors that are resilient to UI changes:
// AVOID: Brittle selectors
cy.get('button:nth-child(2)') // Position can change
cy.get('[style="color: red"]') // Styles can change
// PREFER: More reliable selectors
cy.get('[data-testid="submit"]') // Test IDs
cy.get('[aria-label="Submit"]') // Accessibility attributes
cy.contains('Submit') // Text content
User Actions
Simulate user interactions with elements:
// Mouse actions
cy.get('button').click()
cy.get('.menu-item').dblclick()
cy.get('.draggable').trigger('mousedown')
// Keyboard actions
cy.get('input').type('Hello, World!')
cy.get('input').type('{enter}') // Special characters
cy.get('body').type('{ctrl+a}') // Keyboard shortcuts
// Form interactions
cy.get('[type="checkbox"]').check()
cy.get('[type="radio"]').check('option1')
cy.get('select').select('Option 2')
cy.get('textarea').clear().type('New content')
Handling Hidden Elements
Cypress has specific behavior for visibility:
// By default, Cypress will fail when trying to interact with hidden elements
cy.get('button.hidden').click() // This will fail if button is hidden
// Force interaction with hidden elements if needed
cy.get('button.hidden').click({ force: true })
// Check visibility
cy.get('button').should('be.visible')
cy.get('button').should('not.be.visible')
Real-world example: The New York Times uses data-testid attributes for Cypress tests on their interactive components like the crossword puzzle. This allows their tests to remain stable even as they regularly update the visual design of their website.
Testing Network Requests
Intercepting Network Requests
Cypress can intercept and control network requests:
// Intercept a GET request to monitor it
cy.intercept('GET', '/api/users').as('getUsers')
// Visit page that triggers the request
cy.visit('/users')
// Wait for the request to complete
cy.wait('@getUsers')
Mocking API Responses
Create consistent test environments by stubbing API responses:
// Stub an API response
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
]
}).as('getUsers')
// Test with empty data
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: []
}).as('emptyUsers')
// Test error handling
cy.intercept('GET', '/api/users', {
statusCode: 500,
body: { error: 'Server error' }
}).as('errorUsers')
Dynamic Response Stubbing
Modify requests or responses on the fly:
// Modify response based on request
cy.intercept('GET', '/api/products*', (req) => {
const query = req.query.search
if (query === 'phone') {
req.reply({
statusCode: 200,
body: [{ name: 'iPhone', price: 999 }]
})
} else {
req.reply({
statusCode: 200,
body: []
})
}
})
// Delay response to test loading states
cy.intercept('GET', '/api/data', {
body: { result: 'success' },
delay: 1000 // 1 second delay
})
Verifying Request Details
Inspect network requests for proper behavior:
// Verify request method, URL, and body
cy.intercept('POST', '/api/users', (req) => {
expect(req.body.name).to.equal('John Doe')
expect(req.headers['content-type']).to.include('application/json')
req.reply({ statusCode: 201 })
}).as('createUser')
// Make the request
cy.get('form').submit()
// Wait and verify full request/response
cy.wait('@createUser').then((interception) => {
expect(interception.request.body.email).to.equal('john@example.com')
expect(interception.response.statusCode).to.equal(201)
})
Real-world example: Slack uses Cypress to test their desktop application. They extensively use network interception to test rare states like rate limiting and error handling that would be difficult to reproduce with real API calls.
Managing State and Test Context
Working with Fixtures
Fixtures provide test data in a consistent way:
// Load fixture data
cy.fixture('users.json').then((users) => {
// Use the data in your test
cy.intercept('GET', '/api/users', users)
})
// Modify fixture data before using
cy.fixture('user.json').then((user) => {
user.name = 'Modified Name'
cy.intercept('GET', '/api/user/1', user)
})
// Example users.json fixture
[
{
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
{
"id": 2,
"name": "Jane Smith",
"email": "jane@example.com"
}
]
Custom Commands
Create reusable commands for common operations:
// cypress/support/commands.js
// Command for login
Cypress.Commands.add('login', (email, password) => {
cy.visit('/login')
cy.get('#email').type(email)
cy.get('#password').type(password)
cy.get('button[type="submit"]').click()
cy.url().should('include', '/dashboard')
})
// Command with API login (faster)
Cypress.Commands.add('loginByApi', (email, password) => {
cy.request({
method: 'POST',
url: '/api/login',
body: { email, password }
}).then((response) => {
// Set the returned token in localStorage
window.localStorage.setItem('token', response.body.token)
})
// Visit the protected page directly
cy.visit('/dashboard')
})
// Using custom commands in tests
it('User can view their profile', () => {
// Use custom login command
cy.login('user@example.com', 'password')
// Test after login
cy.get('nav').contains('Profile').click()
cy.url().should('include', '/profile')
})
Sharing Context
Share data between test steps:
// Using closures
it('Creates a post and verifies it', () => {
// Create a variable in scope
let postId
// Create a new post
cy.get('#title').type('Test Post')
cy.get('#content').type('This is a test post')
cy.get('#submit').click()
// Extract the post ID from the redirect URL
cy.url().then((url) => {
postId = url.match(/posts\/(\d+)/)[1]
// Use the ID in subsequent steps
cy.visit(`/posts/${postId}/edit`)
})
})
// Using Cypress.env
it('Creates a post and verifies it', () => {
// Create a new post
cy.get('#submit').click()
// Save the ID in Cypress environment
cy.url().then((url) => {
const postId = url.match(/posts\/(\d+)/)[1]
Cypress.env('postId', postId)
})
// In another test or later steps
it('Edits the created post', () => {
const postId = Cypress.env('postId')
cy.visit(`/posts/${postId}/edit`)
})
})
Real-world example: GitHub uses custom Cypress commands for common workflows like authentication, repository creation, and issue management. This makes their tests more maintainable and readable.
Page Object Pattern in Cypress
The Page Object Pattern centralizes UI element definitions and interactions:
Basic Page Object
// cypress/support/pages/LoginPage.js
class LoginPage {
// URL and element selectors
url = '/login'
emailInput = '#email'
passwordInput = '#password'
submitButton = 'button[type="submit"]'
errorMessage = '.error-message'
// Navigation method
visit() {
cy.visit(this.url)
return this
}
// Action methods
typeEmail(email) {
cy.get(this.emailInput).type(email)
return this
}
typePassword(password) {
cy.get(this.passwordInput).type(password)
return this
}
submit() {
cy.get(this.submitButton).click()
return this
}
// Higher-level method combining actions
login(email, password) {
this.typeEmail(email)
this.typePassword(password)
this.submit()
return this
}
// Assertion methods
shouldShowErrorMessage() {
cy.get(this.errorMessage).should('be.visible')
return this
}
}
export default new LoginPage()
Using Page Objects
// cypress/e2e/login.cy.js
import LoginPage from '../support/pages/LoginPage'
import DashboardPage from '../support/pages/DashboardPage'
describe('Login functionality', () => {
beforeEach(() => {
LoginPage.visit()
})
it('Logs in with valid credentials', () => {
// Use the page object
LoginPage
.typeEmail('user@example.com')
.typePassword('password123')
.submit()
// Verify redirect to dashboard
DashboardPage.verifyLoad()
})
it('Shows error with invalid credentials', () => {
LoginPage
.login('user@example.com', 'wrongpassword')
.shouldShowErrorMessage()
})
})
Component Objects
For reusable components across multiple pages:
// cypress/support/components/NavigationMenu.js
class NavigationMenu {
selectors = {
container: 'nav.main-menu',
homeLink: 'a[href="/"]',
profileLink: 'a[href="/profile"]',
settingsLink: 'a[href="/settings"]'
}
clickHome() {
cy.get(this.selectors.container)
.find(this.selectors.homeLink)
.click()
return this
}
clickProfile() {
cy.get(this.selectors.container)
.find(this.selectors.profileLink)
.click()
return this
}
clickSettings() {
cy.get(this.selectors.container)
.find(this.selectors.settingsLink)
.click()
return this
}
}
export default new NavigationMenu()
Benefits of Page Objects
- Maintainability - Centralize UI element definitions
- Readability - Tests focus on behavior, not implementation
- Reusability - Share common interactions across tests
- Encapsulation - Hide complexity of UI interactions
Real-world example: Spotify uses the Page Object Pattern for testing their web player. Their page objects encapsulate complex interactions like playing tracks, managing playlists, and navigating the library.
Advanced Cypress Features
Visual Testing
Capture and compare screenshots for visual regression testing:
// Basic screenshot
cy.screenshot('login-page')
// Element screenshot
cy.get('.hero-banner').screenshot('hero-section')
// Integrating with Percy for visual diffing
cy.visit('/dashboard')
cy.percySnapshot('Dashboard')
Plugin Ecosystem
Extend Cypress with plugins:
- cypress-file-upload - Upload files in tests
- cypress-xpath - Add XPath selector support
- cypress-mailhog - Test email delivery
- cypress-axe - Accessibility testing
- cypress-real-events - Native browser events
Environment Variables
Configure tests for different environments:
// cypress.config.js
module.exports = defineConfig({
e2e: {
env: {
apiUrl: 'https://api.dev.example.com',
adminUser: 'admin@example.com',
adminPassword: 'supersecret'
}
}
})
// Using environment variables in tests
cy.request({
url: Cypress.env('apiUrl') + '/users',
auth: {
username: Cypress.env('adminUser'),
password: Cypress.env('adminPassword')
}
})
Testing Responsive Design
// Test on different viewports
it('Renders correctly on mobile', () => {
cy.viewport('iphone-x')
cy.visit('/')
cy.get('.mobile-menu').should('be.visible')
cy.get('.desktop-menu').should('not.be.visible')
})
it('Renders correctly on desktop', () => {
cy.viewport(1280, 720)
cy.visit('/')
cy.get('.desktop-menu').should('be.visible')
cy.get('.mobile-menu').should('not.be.visible')
})
Task API for Node.js Operations
// In cypress.config.js
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
on('task', {
// Task to read a file
readFile(filename) {
return fs.readFileSync(filename, 'utf8')
},
// Task to connect to database
async queryDatabase(query) {
const result = await db.execute(query)
return result
}
})
}
}
})
// In tests
cy.task('queryDatabase', 'SELECT * FROM users').then((results) => {
// Use database results in test
})
Best Practices for Cypress Tests
Test Organization
- Structure by feature - Group tests by application features
- Keep tests independent - Each test should run in isolation
- Use descriptive naming - Test names should explain behavior
- Focus on user flows - Test complete user journeys
Performance Optimization
- Use API shortcuts - Bypass UI for setup when possible
- Preserve state between tests - Use
cy.session() - Optimize selector performance - Use specific selectors
- Mock network requests - Speed up tests with stubs
Handling Flakiness
- Use proper waits - Wait for specific conditions
- Implement retry logic - Use test retries for flaky tests
- Control randomness - Set fixed values for dates, random IDs
- Clear application state - Reset between tests
Code Quality
- Use TypeScript - For better type checking and IDE support
- Implement ESLint rules - Enforce consistent patterns
- Apply Page Object Pattern - Centralize UI elements
- Review test code - Apply same standards as application code
Smart Selector Strategy
// Recommended selector priority
// 1. Semantic queries (most stable)
cy.get('button[aria-label="Submit form"]')
cy.get('[role="navigation"]')
// 2. Test-specific attributes
cy.get('[data-testid="login-button"]')
// 3. Forms with labels
cy.get('label:contains("Email")').next('input')
// 4. Content (can be brittle if content changes often)
cy.contains('Sign up')
// AVOID: Implementation details (most brittle)
cy.get('.btn.btn-primary.float-right')
Real-world example: Notion, a popular productivity app, follows these best practices in their Cypress test suite. They use a combination of data-testid attributes and semantic selectors, implement the Page Object Pattern, and use API shortcuts for test setup. This approach allowed them to reduce test suite execution time by 70% while maintaining the same level of coverage.
Practical Exercise
Exercise: Building a Login Test Suite
Let's create a comprehensive Cypress test suite for a login page:
// cypress/e2e/login.cy.js
describe('Login functionality', () => {
beforeEach(() => {
cy.visit('/login')
})
it('successfully logs in with valid credentials', () => {
cy.get('#email').type('user@example.com')
cy.get('#password').type('password123')
cy.get('button[type="submit"]').click()
// Assert redirect to dashboard
cy.url().should('include', '/dashboard')
cy.get('.welcome-message').should('contain', 'Welcome back')
})
it('shows error with invalid credentials', () => {
cy.get('#email').type('user@example.com')
cy.get('#password').type('wrongpassword')
cy.get('button[type="submit"]').click()
// Assert error message
cy.get('.error-message')
.should('be.visible')
.and('contain', 'Invalid credentials')
})
it('shows validation error for empty email', () => {
cy.get('#password').type('password123')
cy.get('button[type="submit"]').click()
// Assert validation message
cy.get('#email-error')
.should('be.visible')
.and('contain', 'Email is required')
})
it('shows validation error for empty password', () => {
cy.get('#email').type('user@example.com')
cy.get('button[type="submit"]').click()
// Assert validation message
cy.get('#password-error')
.should('be.visible')
.and('contain', 'Password is required')
})
it('navigates to forgot password page', () => {
cy.contains('Forgot password?').click()
cy.url().should('include', '/forgot-password')
})
it('navigates to signup page', () => {
cy.contains('Create an account').click()
cy.url().should('include', '/signup')
})
})
Enhance this test suite with:
- Convert it to use the Page Object Pattern
- Add network stubbing for the login API
- Implement custom commands for login
- Add tests for additional scenarios (e.g., remember me checkbox, password visibility toggle)
Summary
- Cypress is a modern E2E testing framework built for the modern web
- It offers developer-friendly features like time travel debugging and automatic waiting
- Cypress has native access to the application, providing better control and visibility
- The framework handles common challenges like waiting for elements and asynchronous operations
- Network interception allows for consistent testing with stubbed responses
- Implement the Page Object Pattern for maintainable, readable tests
- Follow best practices to create reliable, fast, and maintainable test suites
Remember: Cypress helps you build confidence in your application by testing what your users experience, not just what your code does.
Assignment
Create a complete Cypress test suite for an e-commerce website with the following requirements:
- Implement Page Objects for:
- Home page with product listings
- Product detail page
- Shopping cart
- Checkout process (shipping, payment, confirmation)
- User account pages (login, registration, profile)
- Create test specs for:
- User registration and login
- Product browsing and search
- Shopping cart management
- Complete checkout flow
- User account management
- Implement advanced features:
- Custom commands for common actions
- Network stubbing for API responses
- Fixtures for test data
- Visual testing for key pages
- Environment configuration for different test environments
- Follow best practices:
- Proper test organization
- Resilient selectors
- Error handling
- Performance optimization
- Clear documentation
Submit your project with a README.md explaining your testing strategy, folder structure, and instructions for running the tests. Include screenshots of the test results and any visual testing comparisons.
Bonus challenge: Integrate your Cypress tests with a CI/CD pipeline (e.g., GitHub Actions, CircleCI) and configure parallel test execution.