Basics
Cypress is a powerful JavaScript-based end-to-end testing framework designed specifically for modern web applications. This crash course will give you the core knowledge you need for daily testing work, along with a foundation to explore more advanced topics on your own.
What is Cypress and Why Use It?
Cypress is a testing tool that runs directly in the browser, unlike traditional tools like Selenium that operate outside it. This unique approach gives Cypress several key advantages:
- Browser Integration: Runs in the same loop as your application, giving direct access to DOM elements
- Automatic Waiting: No need for arbitrary sleep/wait commands - Cypress automatically waits for elements
- Time Travel Debugging: Takes snapshots at each test step for easy debugging
- Faster & More Reliable: Tests are more consistent due to the architecture
Prerequisites
Before diving into Cypress, you should have:
- Node.js: Version 12 or higher installed
- Code Editor: Visual Studio Code or any preferred editor
- Basic JavaScript Knowledge: Understanding functions, objects, and async operations
- Web Development Basics: Familiarity with HTML and CSS selectors
Installation and Setup
Let’s get Cypress installed and running on your system:
# Create a new project folder
mkdir cypress-project
cd cypress-project
# Initialize a new npm project
npm init -y
# Install Cypress as a dev dependency
npm install cypress --save-dev
# Open Cypress Test Runner
npx cypress open
When you first run Cypress, it will guide you through setup and create a skeleton project structure with example tests. Simply choose “E2E Testing” when prompted during the initial configuration.
Understanding the Cypress Folder Structure
After initialization, your project will have this well-organized structure:
cypress/
├── e2e/ # Your test files go here (.cy.js extension)
├── fixtures/ # Test data files (JSON, CSV, etc.)
├── support/ # Custom commands and configurations
│ ├── commands.js # Define reusable custom commands
│ └── e2e.js # Global configuration for tests
└── downloads/ # Files downloaded during tests are stored here
cypress.config.js # Main configuration file
Writing Your First Test
Now that you understand the basics, let’s create a simple test file in cypress/e2e/first-test.cy.js
:
// Describe block groups related tests
describe('My First Test Suite', () => {
// Each it() is a single test case
it('should visit a website and verify content', () => {
// Visit the target website
cy.visit('https://example.com');
// Verify page has expected content
cy.contains('Example Domain'); // Finds text anywhere on page
// Get element by selector and make assertions
cy.get('h1').should('be.visible'); // Element should be visible
cy.get('h1').should('contain', 'Example Domain'); // Element should contain text
// You can chain assertions
cy.get('p')
.should('exist') // Element exists
.and('contain', 'for illustrative examples'); // Element contains text
});
});
Core Cypress Commands
With your first test written, let’s explore the essential commands you’ll use almost every day. These form the building blocks of all your Cypress tests.
1. Navigation Commands
These commands control browser navigation:
// Visit a URL
cy.visit('https://example.com');
// Go back/forward in browser history
cy.go('back'); // or cy.go(-1)
cy.go('forward'); // or cy.go(1)
// Reload the page
cy.reload();
2. Element Selection Commands
Finding the right elements is crucial for testing:
// Select by CSS selector (ID, class, attribute, etc.)
cy.get('#main-content'); // Select by ID
cy.get('.nav-item'); // Select by class
cy.get('[data-test=submit-button]'); // Select by attribute (preferred for testing)
// Find element containing specific text
cy.contains('Submit'); // Find element with text "Submit"
cy.contains('h2', 'Welcome'); // Find h2 element with text "Welcome"
// Find element within a previously selected element
cy.get('form').find('input[type="email"]');
// Get element by index (when multiple elements match)
cy.get('li').eq(2); // Get the third list item (0-based index)
3. Action Commands
These commands simulate user interactions:
// Click on an element
cy.get('button').click(); // Simple click
cy.get('.menu-item').click({ multiple: true }); // Click all matching elements
cy.get('button').click({ force: true }); // Force click even if element is covered
// Type into an input field
cy.get('input[name="username"]').type('testuser'); // Type text
cy.get('input[name="password"]').type('password123', { sensitive: true }); // Mask in logs
cy.get('input').clear().type('new text'); // Clear first, then type
// Form interactions
cy.get('select').select('Option 2'); // Select dropdown option
cy.get('[type="checkbox"]').check(); // Check a checkbox
cy.get('[type="radio"]').first().check(); // Check first radio button
4. Assertion Commands
Verify that your application behaves as expected:
// Element assertions
cy.get('button').should('exist'); // Element exists
cy.get('button').should('be.visible'); // Element is visible
cy.get('button').should('be.disabled'); // Element is disabled
cy.get('button').should('have.text', 'Submit'); // Exact text match
cy.get('button').should('contain', 'Submit'); // Contains text
cy.get('button').should('have.class', 'primary'); // Has CSS class
cy.get('input').should('have.value', 'test'); // Input has value
// URL assertions
cy.url().should('include', '/dashboard'); // URL contains string
cy.location('pathname').should('eq', '/login'); // Path equals string
// Multiple assertions
cy.get('form')
.should('be.visible')
.and('have.class', 'signup-form')
.find('button')
.should('contain', 'Register');
Running Cypress Tests
Once you’ve written your tests, you’ll need to run them. Cypress offers two main approaches:
1. Interactive Mode (GUI)
npx cypress open
This opens the Cypress Test Runner - a visual interface where you can:
- Select which tests to run
- Watch tests execute in real-time
- View time-travel snapshots
- Debug with the browser’s DevTools
2. Headless Mode (CLI)
# Run all tests headlessly
npx cypress run
# Run specific test file
npx cypress run --spec "cypress/e2e/login.cy.js"
# Run tests in specific browser
npx cypress run --browser chrome
This mode is perfect for CI/CD pipelines and automatically produces:
- Video recordings of test runs
- Screenshots of failed tests
- Detailed command logs
Advanced Testing Patterns
Now that you’ve mastered the basics, let’s explore some more advanced patterns that will help you write more efficient and maintainable tests.
Using Test Hooks for Setup and Teardown
Hooks help you organize common setup and cleanup operations:
describe('User Dashboard', () => {
// Runs once before all tests in this block
before(() => {
// Setup test database or global test state
cy.log('Setting up test data');
});
// Runs before each test
beforeEach(() => {
// Log in before each test
cy.visit('/login');
cy.get('[name="username"]').type('testuser');
cy.get('[name="password"]').type('password123');
cy.get('form').submit();
// Verify login successful
cy.url().should('include', '/dashboard');
});
// Runs after each test
afterEach(() => {
// Clean up after each test
cy.log('Test completed, cleaning up');
});
// Test cases
it('should display user profile information', () => {
cy.get('.profile-card').should('contain', 'testuser');
});
it('should allow editing user settings', () => {
cy.get('.settings-button').click();
// More test steps...
});
});
Working with Test Data (Fixtures)
Fixtures help you separate test data from test logic:
describe('User Profile', () => {
beforeEach(() => {
// Load test data from fixtures folder
cy.fixture('user.json').as('userData');
// Visit page before each test
cy.visit('/profile');
});
it('should fill profile form with fixture data', function () {
// Access fixture data with this.userData
cy.get('#name').type(this.userData.name);
cy.get('#email').type(this.userData.email);
cy.get('#bio').type(this.userData.bio);
cy.get('form').submit();
cy.get('.success-message').should('be.visible');
});
});
Example cypress/fixtures/user.json
:
{
"name": "Test User",
"email": "test@example.com",
"bio": "This is a test biography for the user profile."
}
Intercepting and Mocking Network Requests
Intercepting network requests gives you powerful control over your test environment:
describe('API Testing', () => {
it('should display user data from API', () => {
// Mock API response
cy.intercept('GET', '/api/users/1', {
statusCode: 200,
body: {
id: 1,
name: 'Test User',
email: 'test@example.com',
},
}).as('getUser'); // Assign alias to reference later
// Visit page that will make the API call
cy.visit('/user-profile/1');
// Wait for intercepted request
cy.wait('@getUser');
// Verify data appears on page
cy.get('.user-name').should('contain', 'Test User');
cy.get('.user-email').should('contain', 'test@example.com');
});
it('should handle API error states', () => {
// Mock API error response
cy.intercept('GET', '/api/users/*', {
statusCode: 404,
body: {
error: 'User not found',
},
}).as('getUserError');
cy.visit('/user-profile/999');
cy.wait('@getUserError');
// Verify error message appears
cy.get('.error-message')
.should('be.visible')
.and('contain', 'User not found');
});
});
Page Object Model for Maintainable Tests
The Page Object Model pattern helps keep your tests organized and maintainable by separating page interactions from test logic:
// cypress/support/pages/LoginPage.js
class LoginPage {
// Elements
getUsernameField() {
return cy.get('input[name="username"]');
}
getPasswordField() {
return cy.get('input[name="password"]');
}
getLoginButton() {
return cy.get('button[type="submit"]');
}
getErrorMessage() {
return cy.get('.error-message');
}
// Actions
visit() {
cy.visit('/login');
return this; // For method chaining
}
login(username, password) {
this.getUsernameField().type(username);
this.getPasswordField().type(password);
this.getLoginButton().click();
return this;
}
}
export default new LoginPage();
Using the page object in tests makes your code more readable and maintainable:
// cypress/e2e/login.cy.js
import LoginPage from '../support/pages/LoginPage';
describe('Login Functionality', () => {
beforeEach(() => {
LoginPage.visit();
});
it('should log in with valid credentials', () => {
LoginPage.login('validuser', 'validpass');
cy.url().should('include', '/dashboard');
});
it('should show error with invalid credentials', () => {
LoginPage.login('invaliduser', 'invalidpass');
LoginPage.getErrorMessage()
.should('be.visible')
.and('contain', 'Invalid username or password');
});
});
Cypress Best Practices
Following these best practices will help you write more reliable and maintainable tests:
Use data attributes for test selection
<!-- In your app's HTML --> <button data-cy="submit-button">Submit</button>
// In your test cy.get('[data-cy="submit-button"]').click();
Avoid hard-coded waits
// ❌ Bad practice cy.wait(5000); // Wait 5 seconds and hope element appears // ✅ Good practice cy.get('.loading-spinner').should('not.exist'); // Wait until spinner gone cy.get('.content').should('be.visible'); // Wait until content visible
Keep tests independent
- Each test should set up its own state
- Don’t rely on previous test results
- Use
beforeEach
hooks for common setup
Test what the user sees and interacts with
- Test how elements appear visually
- Focus on user workflows and journeys
- Verify application behavior, not implementation details
Use descriptive test names
// ❌ Unclear test name it('login test', () => { // Test code... }); // ✅ Clear description of what's being tested it('should display error message when password is incorrect', () => { // Test code... });
Debugging Cypress Tests
When tests fail, Cypress offers several powerful debugging tools to help you identify and fix issues:
Time Travel: Click any command in the Command Log to see the application state at that point
Console Debugging:
cy.get('.element').then(($el) => { // Log element to console for inspection console.log($el); });
Pause Execution:
it('test with pause', () => { cy.visit('/app'); cy.pause(); // Pause execution here cy.get('button').click(); });
Screenshots and Videos: Automatically captured for failed tests in headless mode
Debug with Developer Tools:
cy.get('.complicated-element').then(($el) => { // Pause execution in browser dev tools debugger; // Now you can examine $el in the console });
Testing Workflow Visualization
To better understand how all these concepts fit together, here’s a visual representation of the Cypress testing workflow:
graph TD A[Start] --> B[Install Cypress] B --> C[Write Test] C --> D[Run Test] D --> E{Test Passes?} E -->|Yes| F[Commit Code] E -->|No| G[Debug Test] G --> H{Test Issue?} H -->|Yes| C H -->|No| I[Fix Application] I --> D subgraph "Test Development Cycle" C D E G H I end subgraph "Test Organization" J[describe - Test Suite] J --> K[beforeEach - Setup] K --> L[it - Test Case 1] K --> M[it - Test Case 2] L --> N[afterEach - Cleanup] M --> N end subgraph "Command Chain" O[cy.get] O --> P[.should] P --> Q[.and] Q --> R[.click] end
Practical Example: Testing a Login Form
Let’s put everything together with a complete test suite for a login form that demonstrates many of the concepts we’ve covered:
describe('Login Functionality', () => {
beforeEach(() => {
// Visit login page before each test
cy.visit('/login');
// Intercept API calls
cy.intercept('POST', '/api/auth/login').as('loginRequest');
});
it('should display the login form', () => {
// Verify all form elements are visible
cy.get('[data-cy=username]').should('be.visible');
cy.get('[data-cy=password]').should('be.visible');
cy.get('[data-cy=login-button]')
.should('be.visible')
.and('contain', 'Log In');
});
it('should require username and password', () => {
// Try to submit empty form
cy.get('[data-cy=login-button]').click();
// Verify validation messages
cy.get('[data-cy=username-error]')
.should('be.visible')
.and('contain', 'Username is required');
cy.get('[data-cy=password-error]')
.should('be.visible')
.and('contain', 'Password is required');
});
it('should show error for invalid credentials', () => {
// Mock failed login response
cy.intercept('POST', '/api/auth/login', {
statusCode: 401,
body: { message: 'Invalid credentials' },
}).as('failedLogin');
// Fill form with invalid data
cy.get('[data-cy=username]').type('wronguser');
cy.get('[data-cy=password]').type('wrongpass');
cy.get('[data-cy=login-button]').click();
// Wait for request and verify error
cy.wait('@failedLogin');
cy.get('[data-cy=login-error]')
.should('be.visible')
.and('contain', 'Invalid credentials');
});
it('should successfully log in with valid credentials', () => {
// Mock successful login response
cy.intercept('POST', '/api/auth/login', {
statusCode: 200,
body: {
token: 'fake-jwt-token',
user: { id: 1, username: 'testuser' },
},
}).as('successfulLogin');
// Fill form with valid data
cy.get('[data-cy=username]').type('testuser');
cy.get('[data-cy=password]').type('correctpass');
cy.get('[data-cy=login-button]').click();
// Wait for request
cy.wait('@successfulLogin');
// Verify redirect to dashboard
cy.url().should('include', '/dashboard');
// Verify welcome message
cy.get('[data-cy=welcome-message]').should('contain', 'Welcome, testuser');
});
});
The Remaining 15%: Advanced Topics to Explore
Now that you understand the core 85% of Cypress, here are the advanced topics to explore as your testing needs grow:
Custom Commands: Extend Cypress with your own reusable commands
// In cypress/support/commands.js Cypress.Commands.add('login', (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(); }); // In your test cy.login('testuser', 'password123');
Cypress Dashboard Service: Cloud recording, parallelization, and test analytics
- Allows running tests in parallel across multiple machines
- Stores test results, videos, and screenshots
- Provides insights into test performance and flakiness
Visual Testing: Compare screenshots for UI regression testing
- Plugins like
cypress-image-snapshot
for visual comparisons - Integration with services like Percy or Applitools
- Plugins like
Component Testing: Test individual UI components in isolation
- Test React, Vue, Angular components directly
- Faster than full end-to-end tests for component-level behavior
CI/CD Integration: Running Cypress in continuous integration pipelines
- GitHub Actions, CircleCI, Jenkins, etc.
- Parallel test execution
- Test result reporting
API Testing Strategy: Comprehensive backend validation
- Using
cy.request()
for direct API calls - Testing API contracts and schemas
- Integration with OpenAPI/Swagger specifications
- Using
Performance Testing: Basic performance monitoring
- Tracking page load times
- Measuring Core Web Vitals
- Resource loading analysis
Advanced Mocking Techniques:
- Stubbing browser APIs (localStorage, IndexedDB)
- Clock manipulation for time-based testing
- Service worker interception
Cross-Browser Testing: Testing across multiple browsers
- Running tests in Chrome, Firefox, Edge, etc.
- Handling browser-specific behavior differences
Authentication Patterns: Advanced authentication testing
- OAuth flows
- JWT token management
- Session persistence
Summary
This crash course has equipped you with the essential 85% of Cypress knowledge you’ll need for daily testing:
- Installation and basic setup
- Core commands for navigation, selection, and assertions
- Test organization with describe/it blocks and hooks
- Network request interception and mocking
- The Page Object Model pattern for maintainable tests
- Best practices for writing reliable tests
- Debugging techniques for when things go wrong
With this solid foundation, you’re now well-equipped to start writing robust end-to-end tests for your web applications. As your testing needs grow, you can gradually explore the more advanced capabilities we outlined in the remaining 15%.
Remember that the most effective Cypress tests focus on user workflows and behaviors rather than implementation details. This approach will keep your tests resilient to UI changes and provide the most value for your testing efforts.