Stubbing network requests in Cypress isn’t just about faking API responses; it’s about building a robust testing suite that can gracefully handle the unexpected, turning flaky integration tests into predictable unit tests for your frontend.
Imagine this: your app is fetching user data from /api/users/123. Normally, Cypress would hit your actual backend. But if you want to test what happens when that user doesn’t exist, or when the server throws a 500 error, you need to control the network. That’s where cy.intercept() comes in.
Here’s a typical scenario: your dashboard loads, fetching a list of active projects.
// cypress/integration/dashboard.spec.js
describe('Dashboard', () => {
beforeEach(() => {
// Intercept the request to get projects
cy.intercept('GET', '/api/projects', {
statusCode: 200,
body: [
{ id: 1, name: 'Project Alpha', status: 'active' },
{ id: 2, name: 'Project Beta', status: 'active' },
],
}).as('getProjects');
cy.visit('/dashboard');
});
it('should display active projects', () => {
cy.wait('@getProjects'); // Wait for the intercepted request to complete
cy.get('[data-cy=project-list] li').should('have.length', 2);
cy.contains('Project Alpha');
cy.contains('Project Beta');
});
});
This sets up a basic stub. When your app requests /api/projects, Cypress intercepts it and returns a predefined JSON response. cy.wait('@getProjects') ensures your test doesn’t proceed until that specific network call has been made and stubbed.
Now, let’s break down the mental model. cy.intercept() takes a method (GET, POST, etc.), a URL pattern, and an options object. The URL pattern can be a string, a regular expression, or even a function. The options object is where the magic happens:
statusCode: The HTTP status code to return. This is crucial for simulating errors.body: The response body. Can be a string, JSON object, or even a function that generates dynamic data.headers: An object of response headers.delay: Milliseconds to delay the response, simulating slow network conditions.forceNetworkError: A boolean to simulate a complete network failure (e.g., DNS lookup failed).
This control allows you to test the "what ifs" that are hard to replicate with a live backend:
-
Empty States: What if there are no projects?
cy.intercept('GET', '/api/projects', { statusCode: 200, body: [], // Empty array }).as('getProjectsEmpty'); cy.wait('@getProjectsEmpty'); cy.get('[data-cy=empty-state-message]').should('be.visible'); -
Server Errors: What if the API returns a 500 Internal Server Error?
cy.intercept('GET', '/api/projects', { statusCode: 500, body: { error: 'Internal Server Error' }, }).as('getProjectsError'); cy.wait('@getProjectsError'); cy.get('[data-cy=error-message]').should('contain', 'Could not load projects.'); -
Authentication Failures: Testing a 401 Unauthorized response.
cy.intercept('GET', '/api/users/*', { statusCode: 401, body: { message: 'Unauthorized' }, }).as('getUserUnauthorized'); cy.wait('@getUserUnauthorized'); cy.url().should('include', '/login'); // Redirect to login -
Slow API Responses: Simulating a slow loading experience.
cy.intercept('GET', '/api/reports', { statusCode: 200, body: [{ id: 1, name: 'Q1 Report' }], delay: 5000, // 5 second delay }).as('getSlowReports'); cy.wait('@getSlowReports'); // Test that UI handles the delay gracefully
You can also stub POST, PUT, DELETE, and other HTTP methods. For example, creating a new project:
// cypress/integration/create-project.spec.js
describe('Create Project', () => {
it('should create a new project successfully', () => {
cy.intercept('POST', '/api/projects', {
statusCode: 201,
body: { id: 3, name: 'New Project', status: 'active' },
}).as('createProject');
cy.visit('/projects/new');
cy.get('[data-cy=project-name-input]').type('New Project');
cy.get('[data-cy=submit-button]').click();
cy.wait('@createProject');
cy.get('[data-cy=project-list] li').should('have.length', 1);
cy.contains('New Project');
});
});
The real power comes from chaining cy.intercept() calls within a single beforeEach or even within a test case to simulate complex sequences of network interactions. You can also use cy.wait() with multiple aliases if your test depends on several distinct network requests happening.
One subtle, yet incredibly powerful, aspect of cy.intercept() is its ability to dynamically generate response bodies based on the incoming request. Instead of just returning a static body, you can provide a function that receives the request object and returns the desired response. This is fantastic for scenarios where your API returns different data based on query parameters or request payload.
cy.intercept('GET', '/api/items', (req) => {
if (req.query.filter === 'active') {
req.reply({
statusCode: 200,
body: [{ id: 1, name: 'Active Item 1' }],
});
} else {
req.reply({
statusCode: 200,
body: [{ id: 2, name: 'Inactive Item 1' }],
});
}
}).as('getItems');
This allows you to write tests that truly mimic user behavior, like filtering a list, without needing a complex backend setup or relying on potentially unstable test data. You’re not just faking data; you’re dictating the exact network conditions your frontend experiences, isolating it for reliable testing.
The next hurdle you’ll likely encounter is managing state changes across multiple intercepted requests, especially when dealing with mutations that affect subsequent reads.