Testing Tool
Cypress
Overview
Cypress is a modern E2E testing framework that prioritizes developer experience, enabling automated testing in real browsers with excellent debugging features and real-time reload. Built with JavaScript/TypeScript for easy writing, it provides reliable End-to-End testing and integration testing through time-travel debugging, automatic waiting, and network stubbing capabilities. Supporting Electron, Chrome, Firefox, and Edge, it offers a dramatically superior developer experience compared to traditional Selenium-based testing tools through its intuitive UI and real-time test execution confirmation, making it the next-generation web application testing solution.
Details
Cypress 2025 edition has established itself as a developer-first E2E testing framework with a solid position in the market. Its unique architecture, running directly within the browser, enables fast and stable test execution that was impossible with traditional WebDriver-based tools. The time-travel debugging feature allows you to rewind and examine each step during test execution, with detailed verification of snapshots, DOM element states, network requests, and console logs. The automatic waiting (Auto-wait) functionality eliminates the need for explicit wait code, automatically waiting for element visibility, animation completion, and XHR request completion. Network stubbing capabilities enable easy testing of error states and edge cases by mocking API responses. Real-time reloading immediately reflects test code changes, significantly improving iteration speed. It also supports Component Testing, allowing React and Vue component unit tests to run in the same environment. The browser-native execution provides unmatched debugging capabilities and test reliability compared to traditional automation tools.
Key Features
- Time-travel Debugging: Ability to rewind and examine each step of test execution
- Automatic Waiting: Automatic waiting for elements and network requests reduces flaky tests
- Real-time Reload: Immediate reflection of test code changes for fast iteration
- Network Stubbing: API response mocking and error state testing
- Excellent Developer Experience: Intuitive UI with detailed error messages
- Cross-browser Support: Test execution in Chrome, Firefox, Edge, and Electron
Pros and Cons
Pros
- Low learning cost due to intuitive API and excellent developer experience
- Powerful test debugging capabilities through time-travel debugging
- Significant reduction in flaky tests and stable test execution through automatic waiting
- Fast development cycle with real-time test execution confirmation and hot reload
- Comprehensive API behavior testing through network stubbing functionality
- Only JavaScript/TypeScript needed for test writing, no additional language learning required
- Rich plugin ecosystem with active community support
- Unified test environment through integrated Component Testing and E2E Testing
Cons
- Limitations on native browser events due to browser-internal execution
- Complexity and constraints in multi-tab and multi-window testing
- Limited cross-browser testing scope compared to Playwright
- Restricted access to some browser functionality due to security limitations
- Increased memory usage in large test suites
- Limited support for iOS Safari and older browser versions
Reference Links
- Cypress Official Site
- Cypress Official Documentation
- Cypress GitHub Repository
- Cypress Examples
- Cypress Plugin Directory
- Cypress Best Practices
Code Examples
Installation and Setup
# Install Cypress to project
npm install --save-dev cypress
# Initialize Cypress
npx cypress open
# Or run tests headlessly
npx cypress run
# Run tests with specific browser
npx cypress run --browser chrome
npx cypress run --browser firefox
npx cypress run --browser edge
# Initialize project structure
# cypress/ folder and configuration files are automatically generated
# ├── cypress/
# │ ├── e2e/
# │ ├── fixtures/
# │ ├── support/
# │ └── downloads/
# └── cypress.config.js
Basic E2E Testing
// cypress/e2e/basic-test.cy.js
describe('Basic E2E Tests', () => {
beforeEach(() => {
// Common setup before each test
cy.visit('https://example.com')
})
it('should verify page title and content', () => {
// Check page title
cy.title().should('include', 'Example')
// Verify element existence
cy.get('h1').should('be.visible')
cy.get('h1').should('contain.text', 'Example Domain')
// Check multiple elements
cy.get('p').should('have.length.greaterThan', 0)
cy.contains('More information').should('exist')
})
it('should handle link clicking and navigation', () => {
// Click link
cy.contains('More information').click()
// Verify URL change (Cypress automatically waits for page load)
cy.url().should('include', 'iana.org')
// Test back button functionality
cy.go('back')
cy.url().should('eq', 'https://example.com/')
})
it('should verify element states and assertions', () => {
// Check element visibility
cy.get('body').should('be.visible')
// Verify CSS properties
cy.get('h1').should('have.css', 'text-align', 'center')
// Check attribute values
cy.get('a').first().should('have.attr', 'href')
// Detailed text content verification
cy.get('p').first().should('contain.text', 'domain')
cy.get('p').first().should('match', /domain.*examples/)
})
})
Form Interaction and User Input
// cypress/e2e/form-interaction.cy.js
describe('Form Interaction Tests', () => {
beforeEach(() => {
cy.visit('https://example.com/contact')
})
it('should handle basic form input', () => {
// Text input
cy.get('[data-cy="name"]').type('John Doe')
cy.get('[data-cy="email"]').type('[email protected]')
// Textarea input
cy.get('[data-cy="message"]').type('This is a test message.')
// Select dropdown
cy.get('[data-cy="category"]').select('Technical Question')
// Checkbox operations
cy.get('[data-cy="newsletter"]').check()
cy.get('[data-cy="newsletter"]').should('be.checked')
// Radio button selection
cy.get('[data-cy="priority-high"]').check()
cy.get('[data-cy="priority-high"]').should('be.checked')
})
it('should test form submission and validation', () => {
// Test validation errors with empty form
cy.get('[data-cy="submit"]').click()
cy.get('[data-cy="error-message"]').should('be.visible')
cy.get('[data-cy="error-message"]').should('contain', 'required field')
// Fill in correct data
cy.get('[data-cy="name"]').type('Jane Smith')
cy.get('[data-cy="email"]').type('[email protected]')
cy.get('[data-cy="message"]').type('Test message content.')
// Submit form
cy.get('[data-cy="submit"]').click()
// Check success message
cy.get('[data-cy="success-message"]').should('be.visible')
cy.get('[data-cy="success-message"]').should('contain', 'sent successfully')
// Verify URL change
cy.url().should('include', '/success')
})
it('should test file upload functionality', () => {
// File upload (using cypress-file-upload plugin)
const fileName = 'test-image.jpg'
cy.get('[data-cy="file-upload"]').selectFile({
contents: Cypress.Buffer.from('fake image content'),
fileName: fileName,
mimeType: 'image/jpeg',
})
// Verify uploaded file name
cy.get('[data-cy="file-name"]').should('contain', fileName)
// Check preview element visibility
cy.get('[data-cy="file-preview"]').should('be.visible')
})
})
Network Stubbing and API Testing
// cypress/e2e/api-stubbing.cy.js
describe('Network Stubbing Tests', () => {
beforeEach(() => {
// Set up API endpoint stubs
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [
{ id: 1, name: 'John', email: '[email protected]' },
{ id: 2, name: 'Jane', email: '[email protected]' }
]
}).as('getUsers')
})
it('should test normal API response', () => {
cy.visit('/dashboard')
// Wait for API call
cy.wait('@getUsers')
// Verify response data display
cy.get('[data-cy="user-list"]').should('contain', 'John')
cy.get('[data-cy="user-list"]').should('contain', 'Jane')
// Check user count
cy.get('[data-cy="user-item"]').should('have.length', 2)
})
it('should test API error states', () => {
// Set up error response stub
cy.intercept('GET', '/api/users', {
statusCode: 500,
body: { error: 'Internal Server Error' }
}).as('getUsersError')
cy.visit('/dashboard')
cy.wait('@getUsersError')
// Verify error message display
cy.get('[data-cy="error-message"]').should('be.visible')
cy.get('[data-cy="error-message"]').should('contain', 'Server Error')
})
it('should validate request data', () => {
// Stub POST request
cy.intercept('POST', '/api/users', {
statusCode: 201,
body: { id: 3, name: 'New User', email: '[email protected]' }
}).as('createUser')
cy.visit('/users/new')
// Fill form
cy.get('[data-cy="name"]').type('New User')
cy.get('[data-cy="email"]').type('[email protected]')
cy.get('[data-cy="submit"]').click()
// Validate request data
cy.wait('@createUser').then((interception) => {
expect(interception.request.body).to.deep.equal({
name: 'New User',
email: '[email protected]'
})
})
})
it('should test dynamic responses and scenarios', () => {
// Staged response changes
cy.intercept('GET', '/api/status', { statusCode: 202, body: { status: 'pending' } })
.as('getStatusPending')
cy.visit('/process')
cy.get('[data-cy="start-process"]').click()
cy.wait('@getStatusPending')
// Update status
cy.intercept('GET', '/api/status', { statusCode: 200, body: { status: 'completed' } })
.as('getStatusCompleted')
// Test polling behavior
cy.get('[data-cy="refresh"]').click()
cy.wait('@getStatusCompleted')
cy.get('[data-cy="status"]').should('contain', 'completed')
})
})
Custom Commands and Reusable Test Logic
// cypress/support/commands.js
// Define custom commands
Cypress.Commands.add('login', (username, password) => {
cy.session([username, password], () => {
cy.visit('/login')
cy.get('[data-cy="username"]').type(username)
cy.get('[data-cy="password"]').type(password)
cy.get('[data-cy="login-button"]').click()
cy.url().should('include', '/dashboard')
})
})
Cypress.Commands.add('createTodo', (text) => {
cy.get('[data-cy="new-todo"]').type(`${text}{enter}`)
cy.get('[data-cy="todo-list"]').should('contain', text)
})
Cypress.Commands.add('deleteTodo', (text) => {
cy.contains('[data-cy="todo-item"]', text)
.find('[data-cy="delete-button"]')
.click()
cy.get('[data-cy="todo-list"]').should('not.contain', text)
})
// Using custom commands in test files
// cypress/e2e/todo-app.cy.js
describe('Todo Application Tests', () => {
beforeEach(() => {
cy.login('testuser', 'password123')
cy.visit('/todos')
})
it('should create and delete todos', () => {
// Use custom commands
cy.createTodo('Go shopping')
cy.createTodo('Reply to emails')
// Check todo count
cy.get('[data-cy="todo-item"]').should('have.length', 2)
// Delete todo
cy.deleteTodo('Go shopping')
cy.get('[data-cy="todo-item"]').should('have.length', 1)
})
it('should edit and change todo status', () => {
cy.createTodo('Complete task')
// Edit functionality
cy.contains('[data-cy="todo-item"]', 'Complete task')
.find('[data-cy="edit-button"]')
.click()
cy.get('[data-cy="edit-input"]')
.clear()
.type('Task completed{enter}')
// Status change
cy.contains('[data-cy="todo-item"]', 'Task completed')
.find('[data-cy="checkbox"]')
.check()
cy.get('[data-cy="completed-todos"]').should('contain', 'Task completed')
})
})
Cypress Configuration File (cypress.config.js)
// cypress.config.js
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
// Base URL setting
baseUrl: 'http://localhost:3000',
// Viewport size
viewportWidth: 1280,
viewportHeight: 720,
// Test file location
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
// Support file location
supportFile: 'cypress/support/e2e.js',
// Timeout settings
defaultCommandTimeout: 10000,
requestTimeout: 15000,
responseTimeout: 15000,
// Video and screenshot settings
video: true,
screenshotOnRunFailure: true,
// Test result storage
videosFolder: 'cypress/videos',
screenshotsFolder: 'cypress/screenshots',
// Test isolation settings
testIsolation: true,
// Experimental features
experimentalWebKitSupport: true,
setupNodeEvents(on, config) {
// Plugin configuration
on('task', {
// Define custom tasks
log(message) {
console.log(message)
return null
},
// Database reset tasks
resetDb() {
// Database reset processing
return null
}
})
// Browser launch configuration
on('before:browser:launch', (browser = {}, launchOptions) => {
if (browser.family === 'chromium' && browser.name !== 'electron') {
// Auto-open Chrome DevTools
launchOptions.args.push('--auto-open-devtools-for-tabs')
}
return launchOptions
})
return config
},
},
component: {
// Component testing configuration
devServer: {
framework: 'react',
bundler: 'webpack',
},
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
},
// Environment variables
env: {
apiUrl: 'http://localhost:8080/api',
username: 'testuser',
password: 'password123',
},
})
CI/CD Integration (GitHub Actions)
# .github/workflows/cypress.yml
name: Cypress Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
cypress-run:
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chrome, firefox, edge]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Start application
run: |
npm start &
npx wait-on http://localhost:3000
- name: Run Cypress tests
uses: cypress-io/github-action@v6
with:
browser: ${{ matrix.browser }}
record: true
parallel: true
group: 'E2E Tests - ${{ matrix.browser }}'
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload screenshots
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-screenshots-${{ matrix.browser }}
path: cypress/screenshots
- name: Upload videos
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-videos-${{ matrix.browser }}
path: cypress/videos
Debugging and Troubleshooting
// cypress/e2e/debug-example.cy.js
describe('Debugging Features', () => {
it('should demonstrate debugging techniques', () => {
cy.visit('/dashboard')
// Step-by-step execution (development only)
cy.pause() // Pause execution for inspection
// Detailed element verification
cy.get('[data-cy="user-info"]').then(($el) => {
// Output element details to console
console.log('Element:', $el[0])
console.log('Text content:', $el.text())
console.log('HTML:', $el.html())
})
// Debug screenshots
cy.screenshot('debug-point-1')
// Conditional debugging
cy.get('[data-cy="error-message"]').then(($error) => {
if ($error.is(':visible')) {
cy.screenshot('error-state')
cy.log('Error message is visible: ' + $error.text())
}
})
// Custom log output
cy.task('log', 'Passed this point in the test')
// Browser console verification
cy.window().then((win) => {
console.log('Window object:', win)
console.log('Local storage:', win.localStorage)
})
// Detailed network request verification
cy.intercept('GET', '/api/data').as('getData')
cy.get('[data-cy="load-data"]').click()
cy.wait('@getData').then((interception) => {
console.log('Request headers:', interception.request.headers)
console.log('Response body:', interception.response.body)
})
})
it('should handle conditional test execution', () => {
cy.visit('/feature-page')
// Conditional branching after element existence check
cy.get('body').then(($body) => {
if ($body.find('[data-cy="new-feature"]').length > 0) {
// Test for when new feature is enabled
cy.get('[data-cy="new-feature"]').should('be.visible')
cy.get('[data-cy="new-feature"]').click()
} else {
// Test for old feature
cy.get('[data-cy="old-feature"]').should('be.visible')
}
})
})
})