Mocking Network Requests with Cypress Intercept
One of the most powerful features in Cypress is the ability to intercept and control HTTP requests at the browser level. With cy.intercept(), you can stub API responses, spy on network calls to verify they were made with the correct data, simulate network failures, and control response timing—all without running a real backend server or database. This makes tests fast, reliable, and independent of external services.
I've used Cypress intercept in dozens of production applications to test components that fetch data from APIs, forms that submit to backends, and authentication flows. This article covers the patterns that work at scale.
What Is Cypress Intercept?
cy.intercept() sets up a network listener that matches incoming HTTP requests and optionally responds with mocked data. Here's a basic example:
// Intercept GET requests to /api/user
cy.intercept('GET', '/api/user', {
statusCode: 200,
body: {
id: 1,
name: 'John Doe',
email: '[email protected]',
},
})
// Now when your component makes this request, it gets the mocked response
mount(<UserProfile />)
cy.get('h1').should('contain', 'John Doe')
The intercept runs before the actual request leaves the browser, so no real network call is made. Your component thinks it got a response from the server, but it's actually the mocked data.
Basic Intercept Syntax
The basic syntax is:
cy.intercept(method, urlPattern, interceptor)
- method — HTTP method:
'GET','POST','PUT','DELETE','PATCH', etc. You can also pass an array of methods or omit it to match any method. - urlPattern — URL pattern to match. Can be a string, regex, or glob pattern.
- interceptor — Response object or a callback function.
Here are common patterns:
// Match a specific URL
cy.intercept('GET', 'https://api.example.com/users/1')
// Match a path pattern (glob)
cy.intercept('GET', '/api/users/*')
// Match with regex
cy.intercept('GET', /\/api\/users\/\d+/)
// Match any method
cy.intercept('/api/users')
// Match multiple methods
cy.intercept(['GET', 'POST'], '/api/users')
Static Mocked Responses
The simplest use case is returning a static response:
describe('User Profile Component', () => {
it('displays user data from API', () => {
// Intercept the request and return mocked data
cy.intercept('GET', '/api/user', {
statusCode: 200,
body: {
id: 1,
name: 'Jane Smith',
email: '[email protected]',
},
}).as('getUser') // Give it an alias for later assertions
mount(<UserProfile userId={1} />)
// Wait for the intercept and verify it was called
cy.wait('@getUser')
// Verify the component rendered the data
cy.get('h1').should('contain', 'Jane Smith')
cy.get('p').should('contain', '[email protected]')
})
})
The .as('getUser') creates an alias, and cy.wait('@getUser') waits for that specific request to complete. This ensures the request was actually made.
Spying on Request Data
Verify that your component sends the correct data to the API:
describe('Create User Form', () => {
it('submits form with correct data', () => {
cy.intercept('POST', '/api/users', (req) => {
// req.body contains the data being sent
expect(req.body).to.deep.equal({
name: 'Alice',
email: '[email protected]',
})
// Send a mocked response
req.reply({
statusCode: 201,
body: { id: 42, name: 'Alice', email: '[email protected]' },
})
}).as('createUser')
mount(<UserForm />)
cy.get('[data-testid="name-input"]').type('Alice')
cy.get('[data-testid="email-input"]').type('[email protected]')
cy.get('[data-testid="submit-button"]').click()
cy.wait('@createUser')
cy.get('[data-testid="success-message"]').should('contain', 'User created')
})
})
This pattern is powerful for integration testing: your component makes a request, you verify the payload is correct, and you control the response.
Testing Error Scenarios
Test how your component handles API failures:
it('displays error message on API failure', () => {
cy.intercept('GET', '/api/user', {
statusCode: 500,
body: { error: 'Internal Server Error' },
}).as('getUser')
mount(<UserProfile userId={1} />)
cy.wait('@getUser')
// Component should show error message
cy.get('[data-testid="error-message"]').should(
'contain',
'Failed to load user'
)
})
it('retries on network failure', () => {
// First request fails, second succeeds
cy.intercept('GET', '/api/user', (req) => {
if (req.headers['x-attempt'] === '1') {
req.destroy() // Simulate network error
} else {
req.reply({
statusCode: 200,
body: { id: 1, name: 'John' },
})
}
}).as('getUser')
mount(<UserProfile userId={1} withRetry />)
// Verify the component eventually succeeds
cy.get('h1').should('contain', 'John')
})
Simulating Network Delays
Test how your component handles slow networks:
it('shows loading state while fetching', () => {
cy.intercept('GET', '/api/user', (req) => {
// Delay the response by 2 seconds
req.reply((res) => {
res.delay(2000)
res.send({
statusCode: 200,
body: { id: 1, name: 'John' },
})
})
}).as('getUser')
mount(<UserProfile userId={1} />)
// While loading, the loading state should be visible
cy.get('[data-testid="loading-spinner"]').should('be.visible')
// After the delay, data should appear
cy.wait('@getUser')
cy.get('h1').should('contain', 'John')
})
Combining Multiple Intercepts
Test workflows that involve multiple API calls:
it('loads user and their posts', () => {
cy.intercept('GET', '/api/user', {
statusCode: 200,
body: { id: 1, name: 'John' },
}).as('getUser')
cy.intercept('GET', '/api/user/1/posts', {
statusCode: 200,
body: [
{ id: 1, title: 'First Post' },
{ id: 2, title: 'Second Post' },
],
}).as('getPosts')
mount(<UserDashboard userId={1} />)
cy.wait('@getUser')
cy.wait('@getPosts')
cy.get('h1').should('contain', 'John')
cy.get('[data-testid="post"]').should('have.length', 2)
})
Use .as() aliases and cy.wait() to ensure requests complete in the correct order.
Dynamic Responses Based on Request
Sometimes you want to return different responses based on the request:
cy.intercept('GET', '/api/users/*', (req) => {
const userId = req.url.split('/').pop()
const users = {
1: { id: 1, name: 'Alice' },
2: { id: 2, name: 'Bob' },
}
req.reply({
statusCode: 200,
body: users[userId] || { statusCode: 404, body: { error: 'Not found' } },
})
})
This pattern lets you simulate multiple API responses in a single test without writing separate intercepts.
Partial URL Matching
Match patterns without hardcoding the full URL:
// Match any request to api.example.com, any path
cy.intercept('https://api.example.com/**')
// Match /api/v1 or /api/v2 endpoints
cy.intercept(/\/api\/v[12]\/.*/)
// Match requests containing specific query parameters
cy.intercept('/api/users?*')
This is useful for testing components that call the same API endpoint with different parameters.
Practical Example: Testing a Form with Validation
Here's a complete example combining multiple intercept patterns:
// Component: LoginForm
export function LoginForm() {
const [error, setError] = useState('')
const [isLoading, setIsLoading] = useState(false)
const handleSubmit = async (email, password) => {
setIsLoading(true)
try {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
})
if (!res.ok) throw new Error('Login failed')
const data = await res.json()
// Redirect to dashboard
} catch (err) {
setError(err.message)
} finally {
setIsLoading(false)
}
}
return (
<form onSubmit={(e) => {
e.preventDefault()
handleSubmit(/* ... */)
}}>
{/* form fields */}
</form>
)
}
// Test
it('handles login success and failure', () => {
// First test: successful login
cy.intercept('POST', '/api/login', {
statusCode: 200,
body: { token: 'abc123' },
}).as('login')
mount(<LoginForm />)
cy.get('[data-testid="email"]').type('[email protected]')
cy.get('[data-testid="password"]').type('password123')
cy.get('[data-testid="submit"]').click()
cy.wait('@login')
cy.get('[data-testid="success-message"]').should('exist')
})
it('shows error on login failure', () => {
// Simulate server error
cy.intercept('POST', '/api/login', {
statusCode: 401,
body: { error: 'Invalid credentials' },
}).as('login')
mount(<LoginForm />)
cy.get('[data-testid="email"]').type('[email protected]')
cy.get('[data-testid="password"]').type('wrongpassword')
cy.get('[data-testid="submit"]').click()
cy.wait('@login')
cy.get('[data-testid="error-message"]').should('contain', 'Invalid')
})
Key Takeaways
- Use
cy.intercept()to stub HTTP requests and control responses without a real backend. - Return static mocked responses for simple cases, or use callback functions for dynamic responses.
- Spy on request data (method, URL, body, headers) to verify your component sends correct information to APIs.
- Test error scenarios by returning error status codes and messages.
- Simulate network delays and timeouts to test loading states and retries.
- Use
.as()aliases andcy.wait()to ensure requests complete before assertions.
Frequently Asked Questions
How do I intercept requests to an external API like api.stripe.com?
Use the full URL pattern:
cy.intercept('POST', 'https://api.stripe.com/v1/charges', {
statusCode: 200,
body: { id: 'ch_1234', status: 'succeeded' },
})
Cypress intercepts all HTTP requests made by your application, regardless of domain.
Can I intercept GraphQL requests?
Yes. GraphQL requests are POST requests, so intercept the endpoint:
cy.intercept('POST', '/graphql', (req) => {
// req.body.operationName tells you which query/mutation is being sent
if (req.body.operationName === 'GetUser') {
req.reply({
statusCode: 200,
body: {
data: {
user: { id: '1', name: 'John' },
},
},
})
}
})
What if I need to intercept the same URL with different responses in different tests?
Define the intercept before each test, or use beforeEach:
describe('API Tests', () => {
beforeEach(() => {
cy.intercept('GET', '/api/user').as('getUser')
})
it('test 1', () => {
cy.intercept('GET', '/api/user', { body: { name: 'User 1' } })
mount(<Component />)
// ...
})
it('test 2', () => {
cy.intercept('GET', '/api/user', { body: { name: 'User 2' } })
mount(<Component />)
// ...
})
})
Should I use intercept for all API calls or only some?
Use intercept for API calls in component and E2E tests. In E2E tests for staging/production environments, you might use real API calls, but in unit/component tests, always mock. This keeps tests fast and independent.