본문으로 건너뛰기
Previous
Next
Cypress Complete Guide: E2E Testing, Automation & CI/CD I...

Cypress Complete Guide: E2E Testing, Automation & CI/CD Integration

Cypress Complete Guide: E2E Testing, Automation & CI/CD Integration

이 글의 핵심

Cypress is a modern E2E testing framework with automatic waiting, Time Travel debugging, real-time reloading, and built-in network control. Learn commands, assertions, fixtures, intercept, component testing, and CI/CD integration with production-ready patterns.

What is Cypress?

Cypress is a modern end-to-end testing framework designed for web applications. Built from the ground up for the modern web, it provides a complete testing solution with automatic waiting, Time Travel debugging, and real-time reloading.

Real-world experience: Migrating from Selenium to Cypress reduced test writing time by 60% and significantly improved stability through automatic waiting mechanisms.

Why Cypress?

Scenario 1: Selenium is Unreliable

Flaky tests everywhere. Cypress provides automatic waiting for stability.

Scenario 2: Debugging is Difficult

Complex error tracking. Cypress offers Time Travel debugging for easy troubleshooting.

Scenario 3: Complex Setup

WebDriver configuration is tedious. Cypress works out of the box.


Key Features

  • Automatic Waiting: Prevents flaky tests
  • Time Travel: Debug each step visually
  • Real-time Reloading: Instant feedback
  • Screenshots & Videos: Automatic capture on failures
  • API Mocking: Network control with cy.intercept
  • Component Testing: Test React/Vue components
  • TypeScript Support: Full type definitions
  • Dashboard: Test recording and analytics (paid)

Installation & Setup

Install Cypress

npm install -D cypress

Open Cypress

npx cypress open

This launches the Cypress Test Runner and creates initial folder structure:

cypress/
├── e2e/           # E2E test files
├── fixtures/      # Test data
├── support/       # Custom commands
└── downloads/     # Downloaded files

Configuration

// cypress.config.ts
import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    setupNodeEvents(on, config) {
      // implement node event listeners here
    },
    viewportWidth: 1280,
    viewportHeight: 720,
    video: true,
    screenshotOnRunFailure: true,
    defaultCommandTimeout: 10000,
    requestTimeout: 10000,
  },
});

Basic Testing

Login Test Example

// cypress/e2e/login.cy.ts
describe('Login', () => {
  beforeEach(() => {
    cy.visit('/login');
  });

  it('successful login', () => {
    cy.get('input[name="email"]').type('[email protected]');
    cy.get('input[name="password"]').type('password123');
    cy.get('button[type="submit"]').click();
    
    cy.url().should('include', '/dashboard');
    cy.contains('Dashboard').should('be.visible');
  });

  it('failed login shows error', () => {
    cy.get('input[name="email"]').type('[email protected]');
    cy.get('input[name="password"]').type('wrong');
    cy.get('button[type="submit"]').click();
    
    cy.contains('Invalid credentials').should('be.visible');
  });
});

Run Tests

# Open Test Runner
npx cypress open

# Run headless
npx cypress run

# Run specific file
npx cypress run --spec "cypress/e2e/login.cy.ts"

# Run in specific browser
npx cypress run --browser chrome

Commands

cy.visit('/');
cy.visit('/users/123');
cy.go('back');
cy.go('forward');
cy.reload();

Querying

// CSS selectors
cy.get('.submit-btn');
cy.get('#email');
cy.get('[data-testid="user-list"]');

// Text content
cy.contains('Submit');
cy.contains('button', 'Submit');

// Filtering
cy.get('li').first();
cy.get('li').last();
cy.get('li').eq(2);
cy.get('li').filter('.active');

Actions

// Type
cy.get('input').type('Hello World');
cy.get('input').type('{enter}');
cy.get('input').clear();

// Click
cy.get('button').click();
cy.get('button').dblclick();
cy.get('button').rightclick();

// Check/Uncheck
cy.get('input[type="checkbox"]').check();
cy.get('input[type="checkbox"]').uncheck();

// Select
cy.get('select').select('Option 1');
cy.get('select').select(['Option 1', 'Option 2']); // multi-select

// File upload
cy.get('input[type="file"]').selectFile('path/to/file.pdf');

Assertions

// Visibility
cy.get('.alert').should('be.visible');
cy.get('.modal').should('not.exist');

// Text
cy.get('h1').should('have.text', 'Welcome');
cy.get('h1').should('contain', 'Wel');

// Attributes
cy.get('button').should('be.disabled');
cy.get('input').should('have.attr', 'placeholder', 'Email');
cy.get('a').should('have.attr', 'href', '/about');

// CSS
cy.get('.active').should('have.css', 'color', 'rgb(255, 0, 0)');

// Value
cy.get('input').should('have.value', '[email protected]');

// Count
cy.get('.item').should('have.length', 5);

// URL
cy.url().should('include', '/dashboard');
cy.url().should('eq', 'http://localhost:3000/dashboard');

Intercept (API Mocking)

Mock Response

describe('Users', () => {
  beforeEach(() => {
    cy.intercept('GET', '/api/users', {
      statusCode: 200,
      body: [
        { id: 1, name: 'John' },
        { id: 2, name: 'Jane' },
      ],
    }).as('getUsers');
    
    cy.visit('/users');
  });

  it('displays user list', () => {
    cy.wait('@getUsers');
    cy.contains('John').should('be.visible');
    cy.contains('Jane').should('be.visible');
  });
});

Modify Response

cy.intercept('GET', '/api/products', (req) => {
  req.continue((res) => {
    res.body = res.body.slice(0, 5); // Limit to 5 items
  });
});

Simulate Errors

cy.intercept('POST', '/api/users', {
  statusCode: 500,
  body: { error: 'Server error' },
}).as('createUser');

cy.get('button[type="submit"]').click();
cy.wait('@createUser');
cy.contains('Failed to create user').should('be.visible');

Wait for Requests

cy.intercept('GET', '/api/users').as('getUsers');
cy.visit('/users');
cy.wait('@getUsers').then((interception) => {
  expect(interception.response.statusCode).to.equal(200);
  expect(interception.response.body).to.have.length(10);
});

Custom Commands

Define Commands

// cypress/support/commands.ts
declare global {
  namespace Cypress {
    interface Chainable {
      login(email: string, password: string): Chainable<void>;
      logout(): Chainable<void>;
    }
  }
}

Cypress.Commands.add('login', (email: string, password: string) => {
  cy.visit('/login');
  cy.get('input[name="email"]').type(email);
  cy.get('input[name="password"]').type(password);
  cy.get('button[type="submit"]').click();
  cy.url().should('include', '/dashboard');
});

Cypress.Commands.add('logout', () => {
  cy.get('[data-testid="logout-btn"]').click();
  cy.url().should('include', '/login');
});

Use Commands

describe('Dashboard', () => {
  beforeEach(() => {
    cy.login('[email protected]', 'password123');
  });

  it('displays user dashboard', () => {
    cy.contains('Welcome, John').should('be.visible');
  });

  afterEach(() => {
    cy.logout();
  });
});

Fixtures

Create Fixture

// cypress/fixtures/users.json
[
  {
    "id": 1,
    "name": "John Doe",
    "email": "[email protected]"
  },
  {
    "id": 2,
    "name": "Jane Smith",
    "email": "[email protected]"
  }
]

Use Fixture

describe('Users', () => {
  beforeEach(() => {
    cy.fixture('users').then((users) => {
      cy.intercept('GET', '/api/users', users).as('getUsers');
    });
    cy.visit('/users');
  });

  it('displays users from fixture', () => {
    cy.wait('@getUsers');
    cy.contains('John Doe').should('be.visible');
    cy.contains('Jane Smith').should('be.visible');
  });
});

Environment Variables

Set in Configuration

// cypress.config.ts
export default defineConfig({
  e2e: {
    env: {
      apiUrl: 'http://localhost:4000',
      adminEmail: '[email protected]',
    },
  },
});

Use in Tests

cy.visit(Cypress.env('apiUrl'));
cy.get('input[name="email"]').type(Cypress.env('adminEmail'));

Override via Command Line

npx cypress run --env apiUrl=https://staging.example.com

Hooks

Test Hooks

describe('Suite', () => {
  before(() => {
    // Runs once before all tests
    cy.task('seedDatabase');
  });

  beforeEach(() => {
    // Runs before each test
    cy.login('[email protected]', 'password123');
  });

  afterEach(() => {
    // Runs after each test
    cy.clearCookies();
  });

  after(() => {
    // Runs once after all tests
    cy.task('cleanDatabase');
  });

  it('test 1', () => {
    // Test code
  });

  it('test 2', () => {
    // Test code
  });
});

Component Testing

Setup (Cypress 10+)

// cypress.config.ts
export default defineConfig({
  component: {
    devServer: {
      framework: 'react',
      bundler: 'vite',
    },
  },
});

Test React Component

// cypress/component/Button.cy.tsx
import Button from '../../src/components/Button';

describe('Button', () => {
  it('renders correctly', () => {
    cy.mount(<Button>Click Me</Button>);
    cy.contains('Click Me').should('be.visible');
  });

  it('handles click event', () => {
    const onClick = cy.stub().as('onClick');
    cy.mount(<Button onClick={onClick}>Click Me</Button>);
    cy.contains('Click Me').click();
    cy.get('@onClick').should('have.been.calledOnce');
  });

  it('disabled state', () => {
    cy.mount(<Button disabled>Click Me</Button>);
    cy.get('button').should('be.disabled');
  });
});

CI/CD Integration

GitHub Actions

name: Cypress Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  cypress:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: Start server
        run: npm run dev &

      - name: Wait for server
        run: npx wait-on http://localhost:3000

      - name: Run Cypress tests
        uses: cypress-io/github-action@v6
        with:
          browser: chrome
          record: true
        env:
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

      - name: Upload screenshots
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: cypress-screenshots
          path: cypress/screenshots

      - name: Upload videos
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: cypress-videos
          path: cypress/videos

Parallel Execution

jobs:
  cypress:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        machines: [1, 2, 3, 4]
    steps:
      - name: Run Cypress tests
        uses: cypress-io/github-action@v6
        with:
          record: true
          parallel: true
          group: 'E2E Tests'
        env:
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

Best Practices

Stable Selectors

// ❌ Bad: Fragile selectors
cy.get('div > div > button:nth-child(3)').click();

// ✅ Good: Semantic selectors
cy.get('[data-testid="submit-btn"]').click();
cy.contains('button', 'Submit').click();

Avoid Fixed Waits

// ❌ Bad: Fixed wait
cy.wait(5000);
cy.get('.loaded').should('be.visible');

// ✅ Good: Wait for specific condition
cy.get('.loaded', { timeout: 10000 }).should('be.visible');
cy.intercept('GET', '/api/data').as('getData');
cy.wait('@getData');

Test Isolation

// ❌ Bad: Tests depend on each other
it('create user', () => {
  cy.visit('/users/new');
  // ...
});

it('delete user', () => {
  // Depends on previous test
});

// ✅ Good: Independent tests
it('create user', () => {
  cy.task('cleanDatabase');
  cy.visit('/users/new');
  // ...
});

it('delete user', () => {
  cy.task('seedUser', { id: 1, name: 'John' });
  cy.visit('/users/1');
  // ...
});

Debugging

Time Travel

Use Cypress Test Runner to hover over commands and see application state at each step.

Screenshots

cy.screenshot('login-page');
cy.get('.dashboard').screenshot('dashboard');

Videos

Videos are automatically recorded in headless mode. Configure in cypress.config.ts:

export default defineConfig({
  e2e: {
    video: true,
    videoCompression: 32,
  },
});

Debug Command

cy.get('button').debug().click();

Pause Execution

cy.pause();
cy.get('button').click();

Comparison: Cypress vs Playwright

FeatureCypressPlaywright
Browser SupportChrome, Edge, FirefoxChrome, Firefox, WebKit
Multi-TabNoYes
Cross-DomainLimitedFull
LanguageJS/TSJS/TS/Python/Java
Auto-WaitYesYes
Time TravelYesNo (Trace Viewer)
Component TestingYesNo
Parallel ExecutionPaidFree
Learning CurveEasyEasy
EcosystemLargeGrowing

When to use:

  • Cypress: Developer-friendly, React/Vue apps, single-domain
  • Playwright: Cross-browser, multi-tab, multi-domain

Troubleshooting

IssueCauseSolution
Flaky testsRace conditionsUse cy.intercept, avoid cy.wait(time)
Timeout errorsSlow loadingIncrease timeout, optimize app
Element not foundWrong selectorUse Cypress Selector Playground
CI failuresEnvironment diffMatch baseUrl, viewport, timezone
Cross-originMultiple domainsUse cy.origin() in Cypress 9+
Video too largeLong testsReduce compression, delete passed tests

Summary & Checklist

Key Points

  • Automatic Waiting: Prevents flaky tests
  • Time Travel: Debug visually
  • Easy Setup: Works out of the box
  • API Mocking: cy.intercept for network control
  • Component Testing: Test React/Vue components
  • CI-Ready: GitHub Actions, parallel execution

Implementation Checklist

  • Install Cypress and configure
  • Write tests with stable selectors
  • Use cy.intercept for API mocking
  • Add custom commands for repetitive tasks
  • Set up CI/CD pipeline
  • Configure video and screenshots
  • Implement test isolation
  • Enable parallel execution

  • [Playwright Complete Guide](/en/blog/playwright-complete-guide/
  • [Vitest Complete Guide](/en/blog/vitest-complete-guide/
  • [Jest Complete Guide](/en/blog/jest-complete-guide/

Keywords

Cypress, E2E Testing, Automation, CI/CD, Testing, Frontend, TypeScript

Frequently Asked Questions (FAQ)

Q. Can Cypress test mobile apps?

A. No, Cypress tests web applications only. For mobile apps, use Appium or native frameworks. However, Cypress can test responsive designs.

Q. How to handle authentication?

A. Use custom commands (cy.login()) or store session in cy.session() (Cypress 8+) to avoid repeated logins.

Q. Does Cypress support TypeScript?

A. Yes, full TypeScript support with type definitions included.

Q. How to test file downloads?

A. Use cy.readFile() to verify downloaded files in the cypress/downloads/ folder.

Q. Can I run tests in parallel for free?

A. Parallel execution requires Cypress Dashboard (paid). Alternatively, use CI matrix to run shards.

Q. How to test cross-origin scenarios?

A. Use cy.origin() command introduced in Cypress 9.6+ for testing across multiple domains.


For deployment: git addgit commitgit pushnpm run deploy.