Introduction to Cypress
Cypress is a next-generation front-end testing tool built for the modern web. Unlike traditional E2E tools like Selenium, Cypress runs directly in the browser alongside your application, providing fast, reliable, and flake-free testing.
Why Cypress?
- Time Travel: Take snapshots as tests run, travel back in time to any command
- Debuggability: Readable errors and stack traces, Chrome DevTools integration
- Automatic Waiting: No need for explicit waits or sleeps
- Real-Time Reloads: Watch tests execute in real-time as you develop
- Screenshots & Videos: Automatically capture failures and record entire runs
- Network Control: Stub and control network traffic easily
Note: Cypress is designed for testing anything that runs in a browser. It works with any frontend framework or none at all.
Installing Cypress
Install Cypress via npm:
# Install Cypress
npm install --save-dev cypress
# Open Cypress Test Runner
npx cypress open
# Run Cypress tests headlessly
npx cypress run
After installation, Cypress creates this folder structure:
cypress/
├── e2e/ # Test files (.cy.js)
├── fixtures/ # Test data (JSON, CSV, etc.)
├── support/ # Reusable commands and configuration
│ ├── commands.js # Custom commands
│ └── e2e.js # Global hooks and config
└── downloads/ # Downloaded files during tests
Writing Your First Test
Create a test file in cypress/e2e/:
// cypress/e2e/first-test.cy.js
describe('My First Test', () => {
it('visits the app', () => {
cy.visit('https://example.com');
cy.contains('Example Domain');
});
it('has the correct title', () => {
cy.visit('https://example.com');
cy.title().should('eq', 'Example Domain');
});
});
Tip: Use .cy.js or .cy.ts extension for Cypress test files. This helps IDEs provide proper autocomplete and prevents linters from complaining about Cypress globals.
Cypress Fundamentals
Selecting Elements
Cypress provides powerful selectors inspired by jQuery:
// By CSS selector
cy.get('.btn-primary');
cy.get('#username');
// By data attribute (recommended)
cy.get('[data-cy="submit-button"]');
// By text content
cy.contains('Submit');
cy.contains('button', 'Submit');
// Relational queries
cy.get('form').find('input[type="email"]');
cy.get('.header').within(() => {
cy.get('nav').should('be.visible');
});
// First, last, nth
cy.get('li').first();
cy.get('li').last();
cy.get('li').eq(2); // 3rd element (0-indexed)
Best Practice: Use data-cy, data-test, or data-testid attributes for selecting elements. Avoid CSS class names or IDs which may change during styling refactors.
Interacting with Elements
// Typing
cy.get('[data-cy="email-input"]').type('user@example.com');
cy.get('[data-cy="password"]').type('password123{enter}'); // Type and press Enter
// Clicking
cy.get('[data-cy="submit-btn"]').click();
cy.get('[data-cy="menu-item"]').dblclick();
cy.get('[data-cy="context-menu"]').rightclick();
// Checkboxes and radios
cy.get('[data-cy="terms-checkbox"]').check();
cy.get('[data-cy="terms-checkbox"]').uncheck();
cy.get('[data-cy="gender-male"]').check();
// Select dropdowns
cy.get('[data-cy="country-select"]').select('United States');
cy.get('[data-cy="country-select"]').select('US'); // By value
// Clear input
cy.get('[data-cy="search"]').clear();
// Focus and blur
cy.get('[data-cy="input"]').focus();
cy.get('[data-cy="input"]').blur();
Assertions
Cypress includes Chai assertions built-in:
// Implicit assertions (retry automatically)
cy.get('[data-cy="title"]').should('be.visible');
cy.get('[data-cy="title"]').should('have.text', 'Welcome');
cy.get('[data-cy="input"]').should('have.value', 'test');
cy.get('[data-cy="btn"]').should('be.disabled');
cy.get('[data-cy="list"]').should('have.length', 5);
// Chaining assertions
cy.get('[data-cy="error"]')
.should('be.visible')
.and('contain', 'Invalid')
.and('have.class', 'error-message');
// Negative assertions
cy.get('[data-cy="modal"]').should('not.exist');
cy.get('[data-cy="spinner"]').should('not.be.visible');
// Explicit assertions (use then() for more control)
cy.get('[data-cy="count"]').then($el => {
const count = parseInt($el.text());
expect(count).to.be.greaterThan(0);
expect(count).to.be.lessThan(100);
});
Working with Network Requests
Cypress makes it easy to test and stub network requests:
describe('API Tests', () => {
it('waits for API call', () => {
// Intercept HTTP request
cy.intercept('GET', '/api/users').as('getUsers');
cy.visit('/users');
// Wait for request to complete
cy.wait('@getUsers').then((interception) => {
expect(interception.response.statusCode).to.eq(200);
expect(interception.response.body).to.have.length.greaterThan(0);
});
});
it('stubs API response', () => {
// Stub with custom response
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
]
}).as('getUsers');
cy.visit('/users');
cy.wait('@getUsers');
cy.contains('John Doe');
cy.contains('Jane Smith');
});
it('simulates network error', () => {
cy.intercept('GET', '/api/users', {
statusCode: 500,
body: { error: 'Server error' }
});
cy.visit('/users');
cy.contains('Failed to load users');
});
});
Using Fixtures
Store test data in fixtures for reusability:
// cypress/fixtures/users.json
[
{
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
{
"id": 2,
"name": "Jane Smith",
"email": "jane@example.com"
}
]
Load and use fixtures in tests:
describe('Using Fixtures', () => {
it('loads user data from fixture', () => {
cy.fixture('users').then((users) => {
cy.intercept('GET', '/api/users', users);
});
cy.visit('/users');
cy.contains('John Doe');
});
// Alternative: use fixture in intercept directly
it('uses fixture directly', () => {
cy.intercept('GET', '/api/users', { fixture: 'users.json' });
cy.visit('/users');
cy.contains('Jane Smith');
});
});
Custom Commands
Create reusable commands to avoid repetition:
// cypress/support/commands.js
Cypress.Commands.add('login', (email, password) => {
cy.visit('/login');
cy.get('[data-cy="email"]').type(email);
cy.get('[data-cy="password"]').type(password);
cy.get('[data-cy="submit"]').click();
// Wait for redirect
cy.url().should('include', '/dashboard');
});
Cypress.Commands.add('logout', () => {
cy.get('[data-cy="user-menu"]').click();
cy.get('[data-cy="logout"]').click();
cy.url().should('include', '/login');
});
// Add custom assertion
Cypress.Commands.add('shouldBeLoggedIn', () => {
cy.get('[data-cy="user-menu"]').should('exist');
cy.getCookie('auth_token').should('exist');
});
Use custom commands in tests:
describe('User Authentication', () => {
it('logs in successfully', () => {
cy.login('user@example.com', 'password123');
cy.shouldBeLoggedIn();
});
it('logs out successfully', () => {
cy.login('user@example.com', 'password123');
cy.logout();
cy.url().should('include', '/login');
});
});
Working with Forms
Test complex form interactions:
describe('Contact Form', () => {
beforeEach(() => {
cy.visit('/contact');
});
it('submits form with valid data', () => {
cy.get('[data-cy="name"]').type('John Doe');
cy.get('[data-cy="email"]').type('john@example.com');
cy.get('[data-cy="message"]').type('Hello, this is a test message.');
cy.intercept('POST', '/api/contact').as('submitForm');
cy.get('[data-cy="submit"]').click();
cy.wait('@submitForm').its('response.statusCode').should('eq', 200);
cy.contains('Thank you for your message');
});
it('shows validation errors', () => {
cy.get('[data-cy="submit"]').click();
cy.get('[data-cy="name-error"]').should('contain', 'Name is required');
cy.get('[data-cy="email-error"]').should('contain', 'Email is required');
});
it('validates email format', () => {
cy.get('[data-cy="email"]').type('invalid-email');
cy.get('[data-cy="email"]').blur();
cy.get('[data-cy="email-error"]').should('contain', 'Invalid email');
});
});
Handling Async Behavior
Cypress automatically waits for commands and assertions to pass:
describe('Automatic Waiting', () => {
it('waits for element to appear', () => {
cy.visit('/loading-example');
// Cypress retries for up to 4 seconds (default timeout)
cy.get('[data-cy="content"]').should('be.visible');
});
it('uses custom timeout', () => {
cy.visit('/slow-loading');
// Override default timeout
cy.get('[data-cy="content"]', { timeout: 10000 })
.should('be.visible');
});
it('waits for conditions', () => {
cy.visit('/counter');
cy.get('[data-cy="increment"]').click().click().click();
// Cypress retries until assertion passes
cy.get('[data-cy="count"]').should('have.text', '3');
});
});
Note: Cypress commands are queued and executed asynchronously. You don't need async/await or promises - Cypress handles timing automatically.
Testing Authentication Flows
describe('Authentication', () => {
it('completes full login flow', () => {
// Start at home page
cy.visit('/');
// Click login link
cy.get('[data-cy="login-link"]').click();
cy.url().should('include', '/login');
// Fill login form
cy.get('[data-cy="email"]').type('user@example.com');
cy.get('[data-cy="password"]').type('password123');
// Intercept login request
cy.intercept('POST', '/api/auth/login').as('loginRequest');
cy.get('[data-cy="submit"]').click();
// Verify request and response
cy.wait('@loginRequest').then((interception) => {
expect(interception.request.body).to.have.property('email');
expect(interception.response.statusCode).to.eq(200);
});
// Verify redirect and authentication state
cy.url().should('include', '/dashboard');
cy.getCookie('auth_token').should('exist');
cy.get('[data-cy="user-name"]').should('contain', 'Welcome');
});
});
Viewport and Device Testing
Test responsive designs:
describe('Responsive Design', () => {
it('displays mobile menu on small screens', () => {
cy.viewport(375, 667); // iPhone SE
cy.visit('/');
cy.get('[data-cy="mobile-menu-toggle"]').should('be.visible');
cy.get('[data-cy="desktop-nav"]').should('not.be.visible');
});
it('displays desktop nav on large screens', () => {
cy.viewport(1920, 1080);
cy.visit('/');
cy.get('[data-cy="desktop-nav"]').should('be.visible');
cy.get('[data-cy="mobile-menu-toggle"]').should('not.be.visible');
});
// Test predefined viewports
['iphone-x', 'ipad-2', 'macbook-15'].forEach((device) => {
it(`displays correctly on ${device}`, () => {
cy.viewport(device);
cy.visit('/');
cy.get('[data-cy="main-content"]').should('be.visible');
});
});
});
Exercise:
Create a comprehensive E2E test suite for an e-commerce checkout flow:
- Navigate to product listing page
- Search for a specific product
- Click on product to view details
- Add product to cart
- View cart and verify product is there
- Update quantity and verify price calculation
- Proceed to checkout
- Fill shipping and payment information
- Submit order (stub the payment API)
- Verify order confirmation page displays correct details
Use fixtures for product data, create custom commands for reusable actions, and intercept all API calls.
Best Practices
- Use data-cy attributes: Don't rely on CSS classes or IDs that may change
- Create custom commands: Encapsulate common actions (login, logout, etc.)
- Use fixtures: Store test data separately from test logic
- Stub network requests: Make tests faster and more reliable
- Test user journeys: Focus on complete workflows, not isolated features
- Keep tests independent: Each test should run in isolation
- Use beforeEach wisely: Set up common state, but keep tests readable
Common Mistakes:
- Using
.then() like Promises - Cypress commands are queued differently
- Not waiting for network requests before assertions
- Selecting elements by text that may change with translations
- Creating too many small tests instead of testing complete flows
- Not cleaning up state between tests
In the next lesson, we'll explore Playwright, another powerful E2E testing tool with excellent cross-browser support.