Learn Cypress in 10 DaysDay 6: Controlling Network Requests
books.chapter 6Learn Cypress in 10 Days

Day 6: Controlling Network Requests

What You Will Learn Today

  • Basic usage of cy.intercept()
  • Intercepting by HTTP method (GET, POST, PUT, DELETE)
  • Stubbing responses (returning fixed data)
  • Waiting for requests (cy.wait() + alias)
  • Verifying request bodies and headers
  • Simulating error responses (404, 500)
  • Simulating network latency
  • Hands-on: Testing an API-driven UI

What is cy.intercept()?

cy.intercept() is a command that intercepts HTTP requests sent from the browser, allowing you to replace responses or inspect request contents.

flowchart LR
    Browser["Browser"] --> |"Request"| Intercept["cy.intercept()\nIntercept"]
    Intercept --> |"Pass through"| Server["Server"]
    Intercept --> |"Stub response"| Stub["Fixed Response"]
    Server --> |"Response"| Browser
    Stub --> |"Response"| Browser

    style Browser fill:#3b82f6,color:#fff
    style Intercept fill:#f59e0b,color:#fff
    style Server fill:#22c55e,color:#fff
    style Stub fill:#8b5cf6,color:#fff

Why Intercept?

Problem How cy.intercept() Solves It
Unreliable API server Return fixed data with stubs
Difficult test data setup Configure any response you need
Hard to test error cases Easily simulate 404, 500, etc.
Testing network latency Reproduce delays with the delay option
Verifying API calls Inspect request contents

Basic Usage

Simple Intercept

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

// Open the page
cy.visit('/users')

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

URL Pattern Matching

// Exact match
cy.intercept('GET', '/api/users')

// Wildcards
cy.intercept('GET', '/api/users/*')       // /api/users/1, /api/users/abc
cy.intercept('GET', '/api/users/*/posts') // /api/users/1/posts

// Regular expressions
cy.intercept('GET', /\/api\/users\/\d+$/) // /api/users/123

// With query parameters (URL matcher object)
cy.intercept({
  method: 'GET',
  url: '/api/users',
  query: { page: '1', limit: '10' }
})

Intercepting by HTTP Method

GET - Fetching Data

cy.intercept('GET', '/api/users', {
  statusCode: 200,
  body: [
    { id: 1, name: 'John Smith', email: 'john@example.com' },
    { id: 2, name: 'Jane Doe', email: 'jane@example.com' }
  ]
}).as('getUsers')

cy.visit('/users')
cy.wait('@getUsers')

cy.get('.user-card').should('have.length', 2)
cy.get('.user-card').first().should('contain', 'John Smith')

POST - Creating Data

cy.intercept('POST', '/api/users', {
  statusCode: 201,
  body: { id: 3, name: 'Bob Johnson', email: 'bob@example.com' }
}).as('createUser')

cy.get('#name').type('Bob Johnson')
cy.get('#email').type('bob@example.com')
cy.get('button[type="submit"]').click()

cy.wait('@createUser')
cy.get('.success-toast').should('contain', 'User created successfully')

PUT - Updating Data

cy.intercept('PUT', '/api/users/1', {
  statusCode: 200,
  body: { id: 1, name: 'John Smith (Updated)', email: 'john-new@example.com' }
}).as('updateUser')

cy.get('#name').clear().type('John Smith (Updated)')
cy.get('button.save').click()

cy.wait('@updateUser')

DELETE - Deleting Data

cy.intercept('DELETE', '/api/users/1', {
  statusCode: 204,
  body: null
}).as('deleteUser')

cy.get('button.delete').click()
cy.get('.confirm-dialog button.yes').click()

cy.wait('@deleteUser')
cy.get('.user-card').should('have.length', 1)
flowchart TB
    subgraph Methods["HTTP Methods"]
        GET["GET\nFetch Data"]
        POST["POST\nCreate Data"]
        PUT["PUT\nUpdate Data"]
        DELETE["DELETE\nDelete Data"]
    end

    subgraph StatusCodes["Response Codes"]
        S200["200 OK"]
        S201["201 Created"]
        S204["204 No Content"]
    end

    GET --> S200
    POST --> S201
    PUT --> S200
    DELETE --> S204

    style GET fill:#3b82f6,color:#fff
    style POST fill:#22c55e,color:#fff
    style PUT fill:#f59e0b,color:#fff
    style DELETE fill:#ef4444,color:#fff
    style S200 fill:#8b5cf6,color:#fff
    style S201 fill:#8b5cf6,color:#fff
    style S204 fill:#8b5cf6,color:#fff

Stubbing Responses

Stubbing with Fixture Files

Managing test data in external files keeps your test code clean and readable.

// cypress/fixtures/users.json
// [
//   { "id": 1, "name": "John Smith", "email": "john@example.com" },
//   { "id": 2, "name": "Jane Doe", "email": "jane@example.com" }
// ]

cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers')

Dynamic Responses (routeHandler)

cy.intercept('GET', '/api/users/*', (req) => {
  const userId = req.url.split('/').pop()

  req.reply({
    statusCode: 200,
    body: {
      id: Number(userId),
      name: `User ${userId}`,
      email: `user${userId}@example.com`
    }
  })
}).as('getUser')

Setting Response Headers

cy.intercept('GET', '/api/data', {
  statusCode: 200,
  headers: {
    'Content-Type': 'application/json',
    'X-Total-Count': '100',
    'Cache-Control': 'no-cache'
  },
  body: { items: [] }
})

Waiting for and Verifying Requests

Basics of cy.wait() + Alias

cy.intercept('POST', '/api/login').as('loginRequest')

cy.get('#email').type('user@example.com')
cy.get('#password').type('password123')
cy.get('button[type="submit"]').click()

// Wait for the request to complete
cy.wait('@loginRequest').then((interception) => {
  // Verify the request body
  expect(interception.request.body).to.deep.equal({
    email: 'user@example.com',
    password: 'password123'
  })

  // Verify the response
  expect(interception.response.statusCode).to.equal(200)
})

Verifying Request Headers

cy.intercept('GET', '/api/protected').as('protectedRequest')

cy.visit('/protected-page')

cy.wait('@protectedRequest').then((interception) => {
  expect(interception.request.headers).to.have.property('authorization')
  expect(interception.request.headers.authorization).to.include('Bearer')
})

Waiting for Multiple Requests

cy.intercept('GET', '/api/users').as('getUsers')
cy.intercept('GET', '/api/posts').as('getPosts')
cy.intercept('GET', '/api/comments').as('getComments')

cy.visit('/dashboard')

// Wait for multiple requests in order
cy.wait(['@getUsers', '@getPosts', '@getComments'])

// Verify all sections are visible
cy.get('.users-section').should('be.visible')
cy.get('.posts-section').should('be.visible')
cy.get('.comments-section').should('be.visible')

Simulating Error Responses

HTTP Error Codes

// 404 Not Found
cy.intercept('GET', '/api/users/999', {
  statusCode: 404,
  body: { error: 'User not found' }
}).as('getUserNotFound')

cy.visit('/users/999')
cy.wait('@getUserNotFound')
cy.get('.error-message').should('contain', 'User not found')

// 500 Internal Server Error
cy.intercept('POST', '/api/users', {
  statusCode: 500,
  body: { error: 'Internal server error' }
}).as('serverError')

cy.get('form').submit()
cy.wait('@serverError')
cy.get('.error-toast').should('contain', 'A server error occurred')

// 401 Unauthorized
cy.intercept('GET', '/api/protected', {
  statusCode: 401,
  body: { error: 'Unauthorized' }
}).as('unauthorized')

cy.visit('/protected-page')
cy.wait('@unauthorized')
cy.url().should('include', '/login')

Common HTTP Status Codes

Code Name Test Use Case
200 OK Successful response
201 Created Successful creation
204 No Content Successful deletion
400 Bad Request Validation error
401 Unauthorized Authentication error
403 Forbidden Authorization error
404 Not Found Resource not found
409 Conflict Conflict error
422 Unprocessable Entity Validation error
500 Internal Server Error Server error

Simulating Network Latency

The delay Option

// Add a 3-second delay
cy.intercept('GET', '/api/users', {
  statusCode: 200,
  body: [{ id: 1, name: 'John Smith' }],
  delay: 3000  // 3000ms = 3 seconds
}).as('slowRequest')

cy.visit('/users')

// Verify the loading indicator appears
cy.get('.loading-spinner').should('be.visible')

// Verify the loading indicator disappears after data loads
cy.wait('@slowRequest')
cy.get('.loading-spinner').should('not.exist')
cy.get('.user-card').should('be.visible')

Throttling (Bandwidth Limiting)

cy.intercept('GET', '/api/large-data', {
  statusCode: 200,
  body: { data: 'Large amount of data...' },
  throttleKbps: 50  // Limit to 50KB/s
}).as('throttledRequest')
sequenceDiagram
    participant Browser as Browser
    participant Intercept as cy.intercept()
    participant Server as Server

    Browser->>Intercept: GET Request
    Note over Intercept: delay: 3000ms
    Intercept-->>Browser: Loading indicator shown...
    Intercept->>Browser: Response (after 3 seconds)
    Note over Browser: Data displayed

Hands-On: Testing an API-Driven UI

TODO App CRUD Tests

describe('TODO App API Integration Tests', () => {
  const todos = [
    { id: 1, title: 'Go shopping', completed: false },
    { id: 2, title: 'Clean the house', completed: true },
    { id: 3, title: 'Cook dinner', completed: false }
  ]

  beforeEach(() => {
    // Stub the TODO list endpoint
    cy.intercept('GET', '/api/todos', {
      statusCode: 200,
      body: todos
    }).as('getTodos')

    cy.visit('/todos')
    cy.wait('@getTodos')
  })

  it('should display the TODO list', () => {
    cy.get('.todo-item').should('have.length', 3)
    cy.get('.todo-item').first().should('contain', 'Go shopping')
  })

  it('should add a new TODO', () => {
    cy.intercept('POST', '/api/todos', {
      statusCode: 201,
      body: { id: 4, title: 'Study', completed: false }
    }).as('createTodo')

    cy.get('#new-todo').type('Study')
    cy.get('button.add').click()

    cy.wait('@createTodo').then((interception) => {
      expect(interception.request.body).to.deep.equal({
        title: 'Study',
        completed: false
      })
    })
  })

  it('should toggle a TODO completion status', () => {
    cy.intercept('PUT', '/api/todos/1', {
      statusCode: 200,
      body: { id: 1, title: 'Go shopping', completed: true }
    }).as('toggleTodo')

    cy.get('.todo-item').first().find('input[type="checkbox"]').check()

    cy.wait('@toggleTodo').then((interception) => {
      expect(interception.request.body.completed).to.equal(true)
    })
  })

  it('should delete a TODO', () => {
    cy.intercept('DELETE', '/api/todos/1', {
      statusCode: 204
    }).as('deleteTodo')

    cy.get('.todo-item').first().find('button.delete').click()
    cy.wait('@deleteTodo')
  })

  it('should display an error message on server error', () => {
    cy.intercept('POST', '/api/todos', {
      statusCode: 500,
      body: { error: 'Server error' }
    }).as('createTodoError')

    cy.get('#new-todo').type('Test')
    cy.get('button.add').click()

    cy.wait('@createTodoError')
    cy.get('.error-message').should('be.visible')
  })

  it('should show a loading indicator during network latency', () => {
    cy.intercept('GET', '/api/todos', {
      statusCode: 200,
      body: todos,
      delay: 2000
    }).as('slowGetTodos')

    cy.visit('/todos')
    cy.get('.loading').should('be.visible')
    cy.wait('@slowGetTodos')
    cy.get('.loading').should('not.exist')
  })
})

Modifying Requests (req.continue)

// Modify a request before sending it to the server
cy.intercept('GET', '/api/users', (req) => {
  // Add a header
  req.headers['X-Custom-Header'] = 'test-value'

  // Send to the server
  req.continue((res) => {
    // Modify the response
    res.body.push({ id: 99, name: 'Test User' })
    res.send()
  })
})

Summary

Category Command / Option Purpose
Basic cy.intercept(method, url) Intercept a request
Alias .as('alias') Assign a name for waiting and referencing
Wait cy.wait('@alias') Wait for a request to complete
Stub { statusCode, body } Return a fixed response
Fixture { fixture: 'file.json' } Return a response from an external file
Dynamic req.reply() Generate a response dynamically
Error { statusCode: 500 } Simulate an error response
Delay { delay: 3000 } Simulate network latency
Throttle { throttleKbps: 50 } Limit bandwidth
Verification interception.request.body Verify request contents

Key Takeaways

  1. Keep tests independent - Use cy.intercept() to stub APIs and write tests that do not depend on the backend
  2. Master the alias + wait pattern - Assigning aliases to requests and waiting on them is a fundamental Cypress pattern
  3. Use fixture files - Managing test data as fixtures improves reusability
  4. Always test error cases - Go beyond the happy path and test error handling for 404, 500, and other status codes
  5. Test loading UI - Use the delay option to reproduce latency and verify loading state behavior

Exercises

Basics

  1. Use cy.intercept() to stub a GET request and return fixed data
  2. Use cy.wait() to wait for a request to complete and verify the response status code
  3. Create a fixture file and set up an intercept that returns it as the response

Intermediate

  1. Intercept POST, PUT, and DELETE requests and verify the request body for each
  2. Simulate 404 and 500 error responses and test the error display on the page
  3. Simulate network latency and test the appearance and disappearance of a loading indicator

Challenge

  1. Build a test suite that covers all CRUD operations for an API-driven feature
  2. Write a test where the same endpoint returns different responses per request (e.g., success on the first call, error on the second)

References


Next Up

In Day 7, you will learn about custom commands and utilities. You will discover how to define reusable custom commands for frequently used operations, improving the reusability and readability of your test code.