Cypress Testing Framework

Modern end-to-end testing for web applications

Introduction to Cypress

Cypress is a next-generation front-end testing tool built for the modern web. It addresses the pain points developers and QA engineers face when testing modern applications, offering a more developer-friendly and reliable testing experience compared to traditional tools like Selenium.

mindmap root((Cypress)) Key Features All-in-one testing framework Time travel debugging Automatic waiting Real-time reloads Consistent results Screenshots and videos Architecture Runs in same loop as application Direct DOM access Network traffic control Native access to application Benefits Developer friendly Modern JavaScript Fast execution Reliable results Great documentation Active community

Real-world analogy: If traditional E2E testing tools are like remotely controlling a car from the outside, Cypress is like being inside the car with direct access to the steering wheel, pedals, and dashboard. This gives you much more precise control and visibility into what's happening.

Cypress vs. Traditional Testing Tools

Cypress differs from traditional testing tools in several fundamental ways:

Feature Selenium-based Tools Cypress
Architecture External to browser, communicates via WebDriver Runs directly in the browser, same run loop as app
Language Support Multiple languages (Java, Python, Ruby, etc.) JavaScript/TypeScript only
Browser Support All major browsers Chrome, Firefox, Edge, Electron (Safari via WebKit)
Waiting Strategy Explicit waits, implicit waits, sleep Automatic waiting, retries, timeouts
Debugging Logs, screenshots Time travel, real-time reloads, dev tools integration
Setup Complexity Higher (drivers, configurations) Lower (batteries included)
Network Control Limited Full network stubbing/mocking capabilities

Key Advantages of Cypress

graph TD A[Traditional E2E Tools] -->|WebDriver Protocol| B[Browser] C[Cypress] -->|Direct Access| D[Browser] A -->|"Slower, External Commands"| E[Tests] C -->|"Faster, Native Access"| E style C fill:#04B45F,stroke:#04B45F style D fill:#04B45F,stroke:#04B45F

Real-world example: Shopify switched from a Selenium-based stack to Cypress for their storefront testing, reducing their test execution time by 60% and significantly increasing test reliability. The improved developer experience also led to better test coverage as developers became more willing to write and maintain tests.

Setting Up Cypress

Installation

Installing Cypress is straightforward using npm:

// Add Cypress as a development dependency
npm install cypress --save-dev

// Or with Yarn
yarn add cypress --dev

Opening Cypress

// Open Cypress Test Runner
npx cypress open

// Or add to package.json scripts
"scripts": {
  "cypress:open": "cypress open",
  "cypress:run": "cypress run"
}

Initial Setup

When you open Cypress for the first time, it creates a directory structure:

cypress/
├── fixtures/          # Test data
│   └── example.json
├── e2e/               # Test files
│   └── spec.cy.js
├── support/           # Support files
│   ├── commands.js    # Custom commands
│   └── e2e.js         # Global configuration
└── cypress.config.js  # Cypress configuration

Configuration

The main configuration file is cypress.config.js:

// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  // Base URL for cy.visit() commands
  baseUrl: 'http://localhost:3000',
  
  // Viewport dimensions for tests
  viewportWidth: 1280,
  viewportHeight: 720,
  
  e2e: {
    // Folder where tests are located
    specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
    
    // Setup function to run before tests
    setupNodeEvents(on, config) {
      // Plugins and event listeners
    },
  },
  
  // Test retry settings
  retries: {
    runMode: 2,
    openMode: 0
  }
})

TypeScript Support

To use TypeScript with Cypress:

// Install TypeScript
npm install typescript --save-dev

// Create tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["es5", "dom"],
    "types": ["cypress", "node"]
  },
  "include": ["**/*.ts"]
}

Real-world example: Atlassian uses Cypress with TypeScript for testing their cloud products. The type safety helps catch errors early, and the improved autocomplete speeds up test development.

Writing Your First Cypress Test

Let's create a simple test that visits a website and interacts with it:

// cypress/e2e/first-test.cy.js
describe('My First Test', () => {
  it('Visits the home page and clicks a link', () => {
    // Visit the page
    cy.visit('https://example.cypress.io')
    
    // Find and click a link
    cy.contains('type').click()
    
    // Verify the URL changed
    cy.url().should('include', '/commands/actions')
    
    // Interact with a form element
    cy.get('.action-email')
      .type('hello@example.com')
      .should('have.value', 'hello@example.com')
  })
})

Test Structure

Cypress tests use a familiar structure:

Basic Commands

Common Cypress commands include:

// Navigation
cy.visit('/about')           // Visit a page
cy.go('back')                // Navigate back
cy.reload()                  // Reload the page

// Finding elements
cy.get('.button')            // Find by selector
cy.contains('Submit')        // Find by text content
cy.find('input')             // Find within a previous element

// Actions
cy.click()                   // Click an element
cy.type('Hello')             // Type into an input
cy.check()                   // Check a checkbox
cy.select('Option 1')        // Select from a dropdown

// Assertions
cy.should('exist')           // Element exists
cy.should('have.text', 'Hi') // Element has text
cy.should('be.visible')      // Element is visible
cy.should('have.class', 'active') // Element has class

Running Tests

Cypress offers two ways to run tests:

// Interactive mode
npx cypress open

// Headless mode
npx cypress run

// Run specific test file
npx cypress run --spec "cypress/e2e/login.cy.js"
graph TD A[Write Test] --> B[Run in Test Runner] B --> C{Test Passes?} C -->|No| D[Debug using Time Travel] D --> E[Fix Test or Application] E --> B C -->|Yes| F[Run in Headless Mode] F --> G[Add to CI Pipeline]

Core Cypress Concepts

Automatic Waiting

Cypress automatically waits for elements to exist, be visible, and be actionable:

// Cypress will automatically wait up to 4 seconds for:
// 1. The button to exist in the DOM
// 2. The button to be visible (not hidden)
// 3. The button to be enabled (not disabled)
// 4. The button to not be covered by other elements
cy.get('button').click()

// No need for explicit waits like in Selenium:
// Selenium: driver.wait(until.elementIsVisible(button), 5000);

Assertions

Assertions in Cypress are chainable and retried until they pass or timeout:

// Built-in assertions with retry logic
cy.get('.user-name').should('have.text', 'John Doe')

// Multiple assertions in a chain
cy.get('form')
  .should('be.visible')
  .and('have.class', 'signup-form')
  .and('not.have.class', 'submitted')

// Explicit assertions for complex logic
cy.get('.items').then(($items) => {
  expect($items).to.have.length(5)
  expect($items.eq(0)).to.contain('First Item')
})

Chaining and Subject Management

Cypress commands are chainable and pass their subjects:

// Commands form a chain, passing the subject
cy.get('form')                   // Subject: form element
  .find('input[name="email"]')   // Subject: email input inside form
  .type('user@example.com')      // Types into email input
  .should('have.value', 'user@example.com')  // Asserts on email input

// Breaking the chain with .then()
cy.get('form').then(($form) => {
  // Inside .then(), we get a jQuery object, not a Cypress chain
  const action = $form.attr('action')
  expect(action).to.include('/submit')
  
  // Start a new chain with cy.*
  cy.get('input').type('Hello')
})

Aliases for Reuse

Use aliases to reference elements or values across your test:

// Save an element as an alias
cy.get('button.submit').as('submitButton')

// Later in your test
cy.get('@submitButton').click()

// Alias API responses
cy.intercept('GET', '/api/users').as('getUsers')

// Wait for the response
cy.wait('@getUsers').then((interception) => {
  expect(interception.response.statusCode).to.equal(200)
})

Real-world example: Glitch, a collaborative coding platform, uses Cypress's automatic waiting and retry behavior to test their real-time collaborative features. This helps them handle the inherent timing complexity of testing collaborative editing.

Selectors and Element Interaction

Finding Elements

Cypress supports multiple ways to find elements:

// CSS selectors
cy.get('#login-button')
cy.get('.menu-item')
cy.get('header h1')

// Finding by content
cy.contains('Sign Up')
cy.contains('h2', 'Welcome')  // Element type + content

// Combining selectors
cy.get('form').find('input[type="email"]')

// Getting elements by position
cy.get('li').first()
cy.get('li').last()
cy.get('li').eq(2)  // Third item (zero-based index)

Best Practices for Selectors

Use selectors that are resilient to UI changes:

// AVOID: Brittle selectors
cy.get('button:nth-child(2)')  // Position can change
cy.get('[style="color: red"]') // Styles can change

// PREFER: More reliable selectors
cy.get('[data-testid="submit"]')  // Test IDs
cy.get('[aria-label="Submit"]')   // Accessibility attributes
cy.contains('Submit')             // Text content

User Actions

Simulate user interactions with elements:

// Mouse actions
cy.get('button').click()
cy.get('.menu-item').dblclick()
cy.get('.draggable').trigger('mousedown')

// Keyboard actions
cy.get('input').type('Hello, World!')
cy.get('input').type('{enter}')  // Special characters
cy.get('body').type('{ctrl+a}')  // Keyboard shortcuts

// Form interactions
cy.get('[type="checkbox"]').check()
cy.get('[type="radio"]').check('option1')
cy.get('select').select('Option 2')
cy.get('textarea').clear().type('New content')

Handling Hidden Elements

Cypress has specific behavior for visibility:

// By default, Cypress will fail when trying to interact with hidden elements
cy.get('button.hidden').click()  // This will fail if button is hidden

// Force interaction with hidden elements if needed
cy.get('button.hidden').click({ force: true })

// Check visibility
cy.get('button').should('be.visible')
cy.get('button').should('not.be.visible')

Real-world example: The New York Times uses data-testid attributes for Cypress tests on their interactive components like the crossword puzzle. This allows their tests to remain stable even as they regularly update the visual design of their website.

Testing Network Requests

Intercepting Network Requests

Cypress can intercept and control network requests:

// Intercept a GET request to monitor it
cy.intercept('GET', '/api/users').as('getUsers')

// Visit page that triggers the request
cy.visit('/users')

// Wait for the request to complete
cy.wait('@getUsers')

Mocking API Responses

Create consistent test environments by stubbing API responses:

// Stub an API response
cy.intercept('GET', '/api/users', {
  statusCode: 200,
  body: [
    { id: 1, name: 'John Doe' },
    { id: 2, name: 'Jane Smith' }
  ]
}).as('getUsers')

// Test with empty data
cy.intercept('GET', '/api/users', {
  statusCode: 200,
  body: [] 
}).as('emptyUsers')

// Test error handling
cy.intercept('GET', '/api/users', {
  statusCode: 500,
  body: { error: 'Server error' }
}).as('errorUsers')

Dynamic Response Stubbing

Modify requests or responses on the fly:

// Modify response based on request
cy.intercept('GET', '/api/products*', (req) => {
  const query = req.query.search
  
  if (query === 'phone') {
    req.reply({
      statusCode: 200,
      body: [{ name: 'iPhone', price: 999 }]
    })
  } else {
    req.reply({
      statusCode: 200,
      body: []
    })
  }
})

// Delay response to test loading states
cy.intercept('GET', '/api/data', {
  body: { result: 'success' },
  delay: 1000  // 1 second delay
})

Verifying Request Details

Inspect network requests for proper behavior:

// Verify request method, URL, and body
cy.intercept('POST', '/api/users', (req) => {
  expect(req.body.name).to.equal('John Doe')
  expect(req.headers['content-type']).to.include('application/json')
  req.reply({ statusCode: 201 })
}).as('createUser')

// Make the request
cy.get('form').submit()

// Wait and verify full request/response
cy.wait('@createUser').then((interception) => {
  expect(interception.request.body.email).to.equal('john@example.com')
  expect(interception.response.statusCode).to.equal(201)
})
sequenceDiagram participant Browser participant Cypress participant Server Browser->>Cypress: Request to /api/users Cypress->>Cypress: Intercept request Cypress-->>Browser: Stubbed response Note over Browser,Cypress: Real server never receives request

Real-world example: Slack uses Cypress to test their desktop application. They extensively use network interception to test rare states like rate limiting and error handling that would be difficult to reproduce with real API calls.

Managing State and Test Context

Working with Fixtures

Fixtures provide test data in a consistent way:

// Load fixture data
cy.fixture('users.json').then((users) => {
  // Use the data in your test
  cy.intercept('GET', '/api/users', users)
})

// Modify fixture data before using
cy.fixture('user.json').then((user) => {
  user.name = 'Modified Name'
  
  cy.intercept('GET', '/api/user/1', user)
})
// Example users.json fixture
[
  {
    "id": 1,
    "name": "John Doe",
    "email": "john@example.com"
  },
  {
    "id": 2,
    "name": "Jane Smith",
    "email": "jane@example.com"
  }
]

Custom Commands

Create reusable commands for common operations:

// cypress/support/commands.js

// Command for login
Cypress.Commands.add('login', (email, password) => {
  cy.visit('/login')
  cy.get('#email').type(email)
  cy.get('#password').type(password)
  cy.get('button[type="submit"]').click()
  cy.url().should('include', '/dashboard')
})

// Command with API login (faster)
Cypress.Commands.add('loginByApi', (email, password) => {
  cy.request({
    method: 'POST',
    url: '/api/login',
    body: { email, password }
  }).then((response) => {
    // Set the returned token in localStorage
    window.localStorage.setItem('token', response.body.token)
  })
  
  // Visit the protected page directly
  cy.visit('/dashboard')
})
// Using custom commands in tests
it('User can view their profile', () => {
  // Use custom login command
  cy.login('user@example.com', 'password')
  
  // Test after login
  cy.get('nav').contains('Profile').click()
  cy.url().should('include', '/profile')
})

Sharing Context

Share data between test steps:

// Using closures
it('Creates a post and verifies it', () => {
  // Create a variable in scope
  let postId
  
  // Create a new post
  cy.get('#title').type('Test Post')
  cy.get('#content').type('This is a test post')
  cy.get('#submit').click()
  
  // Extract the post ID from the redirect URL
  cy.url().then((url) => {
    postId = url.match(/posts\/(\d+)/)[1]
    
    // Use the ID in subsequent steps
    cy.visit(`/posts/${postId}/edit`)
  })
})

// Using Cypress.env
it('Creates a post and verifies it', () => {
  // Create a new post
  cy.get('#submit').click()
  
  // Save the ID in Cypress environment
  cy.url().then((url) => {
    const postId = url.match(/posts\/(\d+)/)[1]
    Cypress.env('postId', postId)
  })
  
  // In another test or later steps
  it('Edits the created post', () => {
    const postId = Cypress.env('postId')
    cy.visit(`/posts/${postId}/edit`)
  })
})

Real-world example: GitHub uses custom Cypress commands for common workflows like authentication, repository creation, and issue management. This makes their tests more maintainable and readable.

Page Object Pattern in Cypress

The Page Object Pattern centralizes UI element definitions and interactions:

Basic Page Object

// cypress/support/pages/LoginPage.js
class LoginPage {
  // URL and element selectors
  url = '/login'
  emailInput = '#email'
  passwordInput = '#password'
  submitButton = 'button[type="submit"]'
  errorMessage = '.error-message'
  
  // Navigation method
  visit() {
    cy.visit(this.url)
    return this
  }
  
  // Action methods
  typeEmail(email) {
    cy.get(this.emailInput).type(email)
    return this
  }
  
  typePassword(password) {
    cy.get(this.passwordInput).type(password)
    return this
  }
  
  submit() {
    cy.get(this.submitButton).click()
    return this
  }
  
  // Higher-level method combining actions
  login(email, password) {
    this.typeEmail(email)
    this.typePassword(password)
    this.submit()
    return this
  }
  
  // Assertion methods
  shouldShowErrorMessage() {
    cy.get(this.errorMessage).should('be.visible')
    return this
  }
}

export default new LoginPage()

Using Page Objects

// cypress/e2e/login.cy.js
import LoginPage from '../support/pages/LoginPage'
import DashboardPage from '../support/pages/DashboardPage'

describe('Login functionality', () => {
  beforeEach(() => {
    LoginPage.visit()
  })
  
  it('Logs in with valid credentials', () => {
    // Use the page object
    LoginPage
      .typeEmail('user@example.com')
      .typePassword('password123')
      .submit()
    
    // Verify redirect to dashboard
    DashboardPage.verifyLoad()
  })
  
  it('Shows error with invalid credentials', () => {
    LoginPage
      .login('user@example.com', 'wrongpassword')
      .shouldShowErrorMessage()
  })
})

Component Objects

For reusable components across multiple pages:

// cypress/support/components/NavigationMenu.js
class NavigationMenu {
  selectors = {
    container: 'nav.main-menu',
    homeLink: 'a[href="/"]',
    profileLink: 'a[href="/profile"]',
    settingsLink: 'a[href="/settings"]'
  }
  
  clickHome() {
    cy.get(this.selectors.container)
      .find(this.selectors.homeLink)
      .click()
    return this
  }
  
  clickProfile() {
    cy.get(this.selectors.container)
      .find(this.selectors.profileLink)
      .click()
    return this
  }
  
  clickSettings() {
    cy.get(this.selectors.container)
      .find(this.selectors.settingsLink)
      .click()
    return this
  }
}

export default new NavigationMenu()

Benefits of Page Objects

graph TD A[Test] --> B[Page Object] B --> C[DOM Elements] D[Test] --> B E[Test] --> B F[UI Changes] --> B B -.- D B -.- A B -.- E

Real-world example: Spotify uses the Page Object Pattern for testing their web player. Their page objects encapsulate complex interactions like playing tracks, managing playlists, and navigating the library.

Advanced Cypress Features

Visual Testing

Capture and compare screenshots for visual regression testing:

// Basic screenshot
cy.screenshot('login-page')

// Element screenshot
cy.get('.hero-banner').screenshot('hero-section')

// Integrating with Percy for visual diffing
cy.visit('/dashboard')
cy.percySnapshot('Dashboard')

Plugin Ecosystem

Extend Cypress with plugins:

Environment Variables

Configure tests for different environments:

// cypress.config.js
module.exports = defineConfig({
  e2e: {
    env: {
      apiUrl: 'https://api.dev.example.com',
      adminUser: 'admin@example.com',
      adminPassword: 'supersecret'
    }
  }
})

// Using environment variables in tests
cy.request({
  url: Cypress.env('apiUrl') + '/users',
  auth: {
    username: Cypress.env('adminUser'),
    password: Cypress.env('adminPassword')
  }
})

Testing Responsive Design

// Test on different viewports
it('Renders correctly on mobile', () => {
  cy.viewport('iphone-x')
  cy.visit('/')
  cy.get('.mobile-menu').should('be.visible')
  cy.get('.desktop-menu').should('not.be.visible')
})

it('Renders correctly on desktop', () => {
  cy.viewport(1280, 720)
  cy.visit('/')
  cy.get('.desktop-menu').should('be.visible')
  cy.get('.mobile-menu').should('not.be.visible')
})

Task API for Node.js Operations

// In cypress.config.js
module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      on('task', {
        // Task to read a file
        readFile(filename) {
          return fs.readFileSync(filename, 'utf8')
        },
        
        // Task to connect to database
        async queryDatabase(query) {
          const result = await db.execute(query)
          return result
        }
      })
    }
  }
})

// In tests
cy.task('queryDatabase', 'SELECT * FROM users').then((results) => {
  // Use database results in test
})

Best Practices for Cypress Tests

Test Organization

Performance Optimization

Handling Flakiness

Code Quality

Smart Selector Strategy

// Recommended selector priority
// 1. Semantic queries (most stable)
cy.get('button[aria-label="Submit form"]')
cy.get('[role="navigation"]')

// 2. Test-specific attributes
cy.get('[data-testid="login-button"]')

// 3. Forms with labels
cy.get('label:contains("Email")').next('input')

// 4. Content (can be brittle if content changes often)
cy.contains('Sign up')

// AVOID: Implementation details (most brittle)
cy.get('.btn.btn-primary.float-right')

Real-world example: Notion, a popular productivity app, follows these best practices in their Cypress test suite. They use a combination of data-testid attributes and semantic selectors, implement the Page Object Pattern, and use API shortcuts for test setup. This approach allowed them to reduce test suite execution time by 70% while maintaining the same level of coverage.

Practical Exercise

Exercise: Building a Login Test Suite

Let's create a comprehensive Cypress test suite for a login page:

// cypress/e2e/login.cy.js
describe('Login functionality', () => {
  beforeEach(() => {
    cy.visit('/login')
  })
  
  it('successfully logs in with valid credentials', () => {
    cy.get('#email').type('user@example.com')
    cy.get('#password').type('password123')
    cy.get('button[type="submit"]').click()
    
    // Assert redirect to dashboard
    cy.url().should('include', '/dashboard')
    cy.get('.welcome-message').should('contain', 'Welcome back')
  })
  
  it('shows error with invalid credentials', () => {
    cy.get('#email').type('user@example.com')
    cy.get('#password').type('wrongpassword')
    cy.get('button[type="submit"]').click()
    
    // Assert error message
    cy.get('.error-message')
      .should('be.visible')
      .and('contain', 'Invalid credentials')
  })
  
  it('shows validation error for empty email', () => {
    cy.get('#password').type('password123')
    cy.get('button[type="submit"]').click()
    
    // Assert validation message
    cy.get('#email-error')
      .should('be.visible')
      .and('contain', 'Email is required')
  })
  
  it('shows validation error for empty password', () => {
    cy.get('#email').type('user@example.com')
    cy.get('button[type="submit"]').click()
    
    // Assert validation message
    cy.get('#password-error')
      .should('be.visible')
      .and('contain', 'Password is required')
  })
  
  it('navigates to forgot password page', () => {
    cy.contains('Forgot password?').click()
    cy.url().should('include', '/forgot-password')
  })
  
  it('navigates to signup page', () => {
    cy.contains('Create an account').click()
    cy.url().should('include', '/signup')
  })
})

Enhance this test suite with:

  1. Convert it to use the Page Object Pattern
  2. Add network stubbing for the login API
  3. Implement custom commands for login
  4. Add tests for additional scenarios (e.g., remember me checkbox, password visibility toggle)

Summary

Remember: Cypress helps you build confidence in your application by testing what your users experience, not just what your code does.

Assignment

Create a complete Cypress test suite for an e-commerce website with the following requirements:

  1. Implement Page Objects for:
    • Home page with product listings
    • Product detail page
    • Shopping cart
    • Checkout process (shipping, payment, confirmation)
    • User account pages (login, registration, profile)
  2. Create test specs for:
    • User registration and login
    • Product browsing and search
    • Shopping cart management
    • Complete checkout flow
    • User account management
  3. Implement advanced features:
    • Custom commands for common actions
    • Network stubbing for API responses
    • Fixtures for test data
    • Visual testing for key pages
    • Environment configuration for different test environments
  4. Follow best practices:
    • Proper test organization
    • Resilient selectors
    • Error handling
    • Performance optimization
    • Clear documentation

Submit your project with a README.md explaining your testing strategy, folder structure, and instructions for running the tests. Include screenshots of the test results and any visual testing comparisons.

Bonus challenge: Integrate your Cypress tests with a CI/CD pipeline (e.g., GitHub Actions, CircleCI) and configure parallel test execution.