The most surprising thing about mocking API responses in Cypress isn’t that you can do it, but how much it fundamentally changes your testing strategy from "does it work end-to-end?" to "does it work correctly under all conditions?"
Let’s see cy.intercept in action. Imagine a simple app that fetches a list of users from an API and displays them.
// cypress/integration/users.spec.js
describe('User List', () => {
it('displays users from the API', () => {
// This is where the magic happens: we intercept the request
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
}).as('getUsers'); // Give it an alias for waiting
cy.visit('/users'); // Visit the page that triggers the API call
// Wait for the intercepted request to complete
cy.wait('@getUsers');
// Assert that the users are displayed
cy.contains('Alice').should('be.visible');
cy.contains('Bob').should('be.visible');
});
});
This cy.intercept call tells Cypress: "When you see a GET request to /api/users, don’t actually send it. Instead, pretend the server responded with a 200 OK status and this specific JSON body." The .as('getUsers') part is crucial; it lets us use cy.wait('@getUsers') to pause our test until that specific network request (or our mocked version of it) has completed.
The problem cy.intercept solves is the inherent flakiness and slowness of true end-to-end testing. When your tests rely on a live backend, you’re at the mercy of its availability, its speed, and its current state. A slow API call can make your tests drag. An unexpected error on the backend can fail your test even if your frontend code is perfectly fine. cy.intercept creates a controlled environment. You dictate the network responses, making your tests fast, reliable, and repeatable.
Internally, Cypress doesn’t actually block network requests at the browser level. Instead, it injects code into your application that hooks into the browser’s fetch and XMLHttpRequest APIs. When your application makes a network request, Cypress intercepts these calls before they leave the browser. It checks if the request matches any cy.intercept rules you’ve defined. If it does, Cypress returns the mocked response immediately. If not, it allows the request to proceed as normal. This allows you to mock specific endpoints while letting others go through to a real backend if needed, giving you fine-grained control.
The exact levers you control are the HTTP method (GET, POST, PUT, etc.), the URL pattern (which can be a string, a regular expression, or a function), and the response itself. The response can include statusCode, headers, and body. You can even dynamically generate the body using a function, allowing responses to change based on test context or previous actions.
When you intercept a POST request and want to assert something about the data sent, you don’t just mock the response. You can also inspect the request body itself. For example, if you’re sending user data to an endpoint:
// cypress/integration/users.spec.js (continued)
it('creates a new user', () => {
cy.intercept('POST', '/api/users', (req) => {
// The req object has properties like 'url', 'method', and 'body'
expect(req.body.name).to.equal('Charlie');
expect(req.body.email).to.exist;
req.reply({
statusCode: 201,
body: { id: 3, name: 'Charlie', email: 'charlie@example.com' }
});
}).as('createUser');
cy.visit('/users/new');
cy.get('input[name="name"]').type('Charlie');
cy.get('input[name="email"]').type('charlie@example.com');
cy.get('button[type="submit"]').click();
cy.wait('@createUser');
cy.contains('Charlie').should('be.visible');
});
Notice how req.body is directly accessible within the intercept handler. This allows you to assert that your application is sending the correct data to the server, even though the server’s actual response is fully controlled by Cypress. This level of introspection is key to robustly testing form submissions and data mutations.
One common pitfall is forgetting to cy.wait() for your intercepted requests, leading to tests that pass because the UI eventually updates, but not because the data was correctly processed. Always pair cy.intercept with cy.wait() when you need to ensure a specific network interaction has completed before proceeding with assertions.
The next step is to explore how to chain cy.intercept calls to simulate complex multi-step API interactions.