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

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')
      }
    })
  })
})