Cypress

Testing ToolJavaScriptE2EComponent TestingUnit TestingFrontendDeveloper Experience

GitHub Overview

cypress-io/cypress

Fast, easy and reliable testing for anything that runs in a browser.

Stars48,773
Watchers602
Forks3,311
Created:March 4, 2015
Language:TypeScript
License:MIT License

Topics

angular-testing-librarycomponent-testingcypresscypress-cloudcypress-schematicend-to-end-testingreact-testing-librarysvelte-testing-librarytest-replaytesting-frameworktesting-librarytesting-toolvue-testing-library

Star History

cypress-io/cypress Star History
Data as of: 7/20/2025, 02:18 AM

Testing Tool

Cypress

Overview

Cypress is a modern testing framework that supports "component testing and unit testing" in addition to traditional E2E testing. It provides the ability to test React, Vue, Angular, and other framework components in isolation, significantly improving development efficiency through excellent developer experience and real-time debugging capabilities. With the cy.mount() command for component mounting, automatic test re-execution, and intuitive test syntax, it has established itself as one of the most user-friendly unit testing tools for frontend developers.

Details

Cypress Unit Testing (Component Testing) was released as beta in 2022 and officially launched in 2023, extending the excellent developer experience cultivated in traditional E2E testing to the unit test level. By actually rendering and testing components in a real browser environment, comprehensive testing including DOM manipulation, event handling, and styling is possible. It supports major frameworks such as React, Vue, Angular, and Svelte, and also handles framework-specific features (props, slots, signals, etc.). Popular E2E test features like Test Runner UI for real-time execution monitoring, automatic screenshots, time-travel debugging, and network stubbing are also available for unit testing. You can start immediately without configuration, with seamless integration with Webpack and Vite, complete TypeScript support, and rich assertion capabilities that naturally integrate into modern frontend development workflows.

Key Features

  • Real Browser Testing: Execute and validate components in actual browser environments
  • cy.mount(): Dedicated command for mounting components independently
  • Real-time Debugging: Real-time monitoring of DOM and network during test execution
  • Framework Support: Support for major frameworks including React, Vue, Angular, and Svelte
  • Auto Re-execution: Automatic test execution and Hot Reload on file changes
  • Visual Debugging: Time-travel functionality and automatic screenshot generation

Pros and Cons

Pros

  • Same intuitive API and excellent developer experience as E2E testing
  • High reliability and complete DOM manipulation support through real browser environment
  • Efficient debugging and troubleshooting via visual Test Runner UI
  • Complete support for framework-specific features (props, events, slots, etc.)
  • Immediate start without configuration and easy integration with Webpack/Vite
  • Rich assertion capabilities and network mocking/stubbing features
  • Complete TypeScript support and type-safe syntax
  • Unified writing approach for both E2E and component testing

Cons

  • Ecosystem still developing as it's a relatively new feature
  • Slightly slower test execution compared to traditional tools like Jest
  • High resource consumption due to real browser execution, unsuitable for large test suites
  • Learning cost for Cypress and migration cost from existing tests
  • Somewhat complex headless execution setup on CI servers
  • No support for Node.js environment unit tests, frontend-only

Reference Links

Code Examples

Installation and Setup

# Install Cypress
npm install --save-dev cypress

# Initialize Cypress configuration
npx cypress open

# Select component testing configuration
# → Choose "Component Testing"
# → Select framework (React, Vue, Angular, etc.)
# → Confirm automatic configuration file generation

# Run component tests via CLI
npx cypress run --component

# Run specific test file
npx cypress run --component --spec "src/**/*.cy.{js,jsx,ts,tsx}"

Basic Component Testing (React)

// components/Button.cy.jsx
import React from 'react'
import Button from './Button'

describe('<Button />', () => {
  it('mounts and verifies basic display', () => {
    cy.mount(<Button>Click me!</Button>)
    cy.get('button').should('contain.text', 'Click me!')
    cy.get('button').should('be.visible')
  })

  it('props are passed correctly', () => {
    cy.mount(
      <Button variant="primary" disabled={true}>
        Primary Button
      </Button>
    )
    
    cy.get('button')
      .should('have.class', 'btn-primary')
      .should('be.disabled')
      .should('contain.text', 'Primary Button')
  })

  it('click event is triggered', () => {
    const onClickSpy = cy.spy().as('onClickSpy')
    cy.mount(<Button onClick={onClickSpy}>Click me</Button>)
    
    cy.get('button').click()
    cy.get('@onClickSpy').should('have.been.called')
  })

  it('multiple click test', () => {
    const onClickSpy = cy.spy().as('onClickSpy')
    cy.mount(<Button onClick={onClickSpy}>Multi Click</Button>)
    
    cy.get('button').click().click().click()
    cy.get('@onClickSpy').should('have.been.calledThrice')
  })
})

Component Testing (Vue)

// components/Counter.cy.js
import Counter from './Counter.vue'

describe('<Counter />', () => {
  it('initial value displays correctly', () => {
    cy.mount(Counter, {
      props: {
        initialValue: 10
      }
    })
    
    cy.get('[data-cy=counter-value]').should('contain.text', '10')
  })

  it('increment and decrement functionality', () => {
    cy.mount(Counter)
    
    // Check initial value
    cy.get('[data-cy=counter-value]').should('contain.text', '0')
    
    // Increment
    cy.get('[data-cy=increment-btn]').click()
    cy.get('[data-cy=counter-value]').should('contain.text', '1')
    
    // Decrement
    cy.get('[data-cy=decrement-btn]').click()
    cy.get('[data-cy=counter-value]').should('contain.text', '0')
  })

  it('event emission test', () => {
    const onChangeSpy = cy.spy().as('onChangeSpy')
    cy.mount(Counter, {
      props: {
        onChange: onChangeSpy
      }
    })
    
    cy.get('[data-cy=increment-btn]').click()
    cy.get('@onChangeSpy').should('have.been.calledWith', 1)
  })

  it('slot content test', () => {
    cy.mount(Counter, {
      slots: {
        default: '<span>Custom Content</span>',
        footer: 'Footer Content'
      }
    })
    
    cy.get('span').should('contain.text', 'Custom Content')
    cy.contains('Footer Content').should('be.visible')
  })
})

Component Testing (Angular)

// components/stepper.component.cy.ts
import { StepperComponent } from './stepper.component'
import { createOutputSpy } from 'cypress/angular'

describe('StepperComponent', () => {
  it('mounts and displays initial value', () => {
    cy.mount(StepperComponent)
    cy.get('[data-cy=counter]').should('contain.text', '0')
  })

  it('input property test', () => {
    cy.mount(StepperComponent, {
      componentProperties: {
        count: 100
      }
    })
    
    cy.get('[data-cy=counter]').should('contain.text', '100')
  })

  it('output event test', () => {
    cy.mount(StepperComponent, {
      componentProperties: {
        change: createOutputSpy('changeSpy')
      }
    })
    
    cy.get('[data-cy=increment]').click()
    cy.get('@changeSpy').should('have.been.calledWith', 1)
  })

  it('complex operation test scenario', () => {
    const changeSpy = createOutputSpy('changeSpy')
    cy.mount(StepperComponent, {
      componentProperties: {
        count: 50,
        change: changeSpy
      }
    })
    
    // Start from 50
    cy.get('[data-cy=counter]').should('contain.text', '50')
    
    // Increment 3 times
    cy.get('[data-cy=increment]').click().click().click()
    cy.get('[data-cy=counter]').should('contain.text', '53')
    cy.get('@changeSpy').should('have.been.calledWith', 53)
    
    // Decrement 2 times
    cy.get('[data-cy=decrement]').click().click()
    cy.get('[data-cy=counter]').should('contain.text', '51')
  })
})

Form Component Testing

// components/LoginForm.cy.jsx
import LoginForm from './LoginForm'

describe('<LoginForm />', () => {
  it('form elements display correctly', () => {
    cy.mount(<LoginForm />)
    
    cy.get('input[name="username"]').should('be.visible')
    cy.get('input[name="password"]').should('have.attr', 'type', 'password')
    cy.get('button[type="submit"]').should('contain.text', 'Login')
  })

  it('validation functionality test', () => {
    cy.mount(<LoginForm />)
    
    // Submit with empty state
    cy.get('button[type="submit"]').click()
    cy.get('.error-message').should('contain.text', 'Username is required')
    
    // Enter username only
    cy.get('input[name="username"]').type('testuser')
    cy.get('button[type="submit"]').click()
    cy.get('.error-message').should('contain.text', 'Password is required')
  })

  it('normal login flow test', () => {
    const onSubmitSpy = cy.spy().as('onSubmitSpy')
    cy.mount(<LoginForm onSubmit={onSubmitSpy} />)
    
    // Form input
    cy.get('input[name="username"]').type('validuser')
    cy.get('input[name="password"]').type('validpass123')
    
    // Submit execution
    cy.get('button[type="submit"]').click()
    
    // Verify submit with expected data
    cy.get('@onSubmitSpy').should('have.been.calledWith', {
      username: 'validuser',
      password: 'validpass123'
    })
  })

  it('password show/hide functionality', () => {
    cy.mount(<LoginForm showPasswordToggle={true} />)
    
    // Initial state is password type
    cy.get('input[name="password"]').should('have.attr', 'type', 'password')
    
    // Click show button
    cy.get('[data-cy=toggle-password]').click()
    cy.get('input[name="password"]').should('have.attr', 'type', 'text')
    
    // Click again to hide
    cy.get('[data-cy=toggle-password]').click()
    cy.get('input[name="password"]').should('have.attr', 'type', 'password')
  })
})

Asynchronous Processing and Mocking

// components/UserProfile.cy.jsx
import UserProfile from './UserProfile'

describe('<UserProfile />', () => {
  it('user data loading test', () => {
    // Mock API call
    cy.intercept('GET', '/api/users/123', {
      fixture: 'user.json'
    }).as('getUser')
    
    cy.mount(<UserProfile userId={123} />)
    
    // Check loading state
    cy.get('[data-cy=loading]').should('be.visible')
    
    // Wait for API call
    cy.wait('@getUser')
    
    // Check data display
    cy.get('[data-cy=user-name]').should('contain.text', 'Test User')
    cy.get('[data-cy=user-email]').should('contain.text', '[email protected]')
    cy.get('[data-cy=loading]').should('not.exist')
  })

  it('error handling test', () => {
    // Mock error response
    cy.intercept('GET', '/api/users/123', {
      statusCode: 404,
      body: { error: 'User not found' }
    }).as('getUserError')
    
    cy.mount(<UserProfile userId={123} />)
    
    cy.wait('@getUserError')
    
    // Check error message
    cy.get('[data-cy=error-message]')
      .should('be.visible')
      .should('contain.text', 'User not found')
  })

  it('reload functionality test', () => {
    cy.intercept('GET', '/api/users/123', {
      fixture: 'user.json'
    }).as('getUser')
    
    cy.mount(<UserProfile userId={123} />)
    cy.wait('@getUser')
    
    // Click reload button
    cy.get('[data-cy=reload-btn]').click()
    
    // Verify API is called again
    cy.wait('@getUser')
    cy.get('[data-cy=user-name]').should('contain.text', 'Test User')
  })
})

Custom Hooks and Utility Function Testing

// utils/math.cy.js - Pure function testing
import { add, subtract, multiply, divide, calculateTax } from '../src/utils/math'

describe('Math Utils', () => {
  context('Basic Operations', () => {
    it('addition test', () => {
      expect(add(2, 3)).to.equal(5)
      expect(add(-1, 1)).to.equal(0)
      expect(add(0.1, 0.2)).to.be.closeTo(0.3, 0.001)
    })

    it('subtraction test', () => {
      expect(subtract(5, 3)).to.equal(2)
      expect(subtract(1, 1)).to.equal(0)
      expect(subtract(-5, -3)).to.equal(-2)
    })

    it('multiplication test', () => {
      expect(multiply(3, 4)).to.equal(12)
      expect(multiply(-2, 5)).to.equal(-10)
      expect(multiply(0, 100)).to.equal(0)
    })

    it('division test', () => {
      expect(divide(10, 2)).to.equal(5)
      expect(divide(7, 2)).to.equal(3.5)
      expect(() => divide(5, 0)).to.throw('Division by zero')
    })
  })

  context('Tax Calculation', () => {
    it('consumption tax calculation (10%)', () => {
      expect(calculateTax(1000, 0.1)).to.equal(100)
      expect(calculateTax(250, 0.1)).to.equal(25)
    })

    it('decimal handling test', () => {
      expect(calculateTax(333, 0.1)).to.be.closeTo(33.3, 0.1)
    })

    it('error case test', () => {
      expect(() => calculateTax(-100, 0.1)).to.throw('Amount must be positive')
      expect(() => calculateTax(100, -0.1)).to.throw('Tax rate must be positive')
    })
  })
})

Configuration and Best Practices

// cypress/support/component.js - Support file
import './commands'
import '../../src/styles/index.css'

// Custom mount for React Router
Cypress.Commands.add('mount', (component, options = {}) => {
  const { routerProps = { initialEntries: ['/'] }, ...mountOptions } = options
  
  const wrapped = (
    <BrowserRouter {...routerProps}>
      {component}
    </BrowserRouter>
  )

  return mount(wrapped, mountOptions)
})

// Custom mount for Redux
Cypress.Commands.add('mountWithRedux', (component, options = {}) => {
  const { reduxStore = getStore(), ...mountOptions } = options
  
  const wrapped = (
    <Provider store={reduxStore}>
      {component}
    </Provider>
  )

  return mount(wrapped, mountOptions)
})

// cypress.config.js - Configuration file
module.exports = {
  component: {
    devServer: {
      framework: 'react',
      bundler: 'vite', // or 'webpack'
    },
    specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
    viewportWidth: 1280,
    viewportHeight: 720,
    video: false,
    screenshotOnRunFailure: true,
  },
  
  e2e: {
    baseUrl: 'http://localhost:3000',
    supportFile: 'cypress/support/e2e.js',
  }
}