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
Navigation
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
| Feature | Cypress | Playwright |
|---|---|---|
| Browser Support | Chrome, Edge, Firefox | Chrome, Firefox, WebKit |
| Multi-Tab | No | Yes |
| Cross-Domain | Limited | Full |
| Language | JS/TS | JS/TS/Python/Java |
| Auto-Wait | Yes | Yes |
| Time Travel | Yes | No (Trace Viewer) |
| Component Testing | Yes | No |
| Parallel Execution | Paid | Free |
| Learning Curve | Easy | Easy |
| Ecosystem | Large | Growing |
When to use:
- Cypress: Developer-friendly, React/Vue apps, single-domain
- Playwright: Cross-browser, multi-tab, multi-domain
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| Flaky tests | Race conditions | Use cy.intercept, avoid cy.wait(time) |
| Timeout errors | Slow loading | Increase timeout, optimize app |
| Element not found | Wrong selector | Use Cypress Selector Playground |
| CI failures | Environment diff | Match baseUrl, viewport, timezone |
| Cross-origin | Multiple domains | Use cy.origin() in Cypress 9+ |
| Video too large | Long tests | Reduce 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
Related Articles
- [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 add → git commit → git push → npm run deploy.