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
- Keep tests independent - Use cy.intercept() to stub APIs and write tests that do not depend on the backend
- Master the alias + wait pattern - Assigning aliases to requests and waiting on them is a fundamental Cypress pattern
- Use fixture files - Managing test data as fixtures improves reusability
- Always test error cases - Go beyond the happy path and test error handling for 404, 500, and other status codes
- Test loading UI - Use the delay option to reproduce latency and verify loading state behavior
Exercises
Basics
- Use
cy.intercept()to stub a GET request and return fixed data - Use
cy.wait()to wait for a request to complete and verify the response status code - Create a fixture file and set up an intercept that returns it as the response
Intermediate
- Intercept POST, PUT, and DELETE requests and verify the request body for each
- Simulate 404 and 500 error responses and test the error display on the page
- Simulate network latency and test the appearance and disappearance of a loading indicator
Challenge
- Build a test suite that covers all CRUD operations for an API-driven feature
- 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.