Cypress tests can feel like they’re running in slow motion because they’re often waiting for real network requests, which are inherently unpredictable and slow.

Let’s watch Cypress in action, but first, let’s set the stage. Imagine we have a simple app that fetches user data from an API.

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>User Data</title>
</head>
<body>
  <div id="user-info">Loading user...</div>
  <script src="app.js"></script>
</body>
</html>
// app.js
fetch('/api/user')
  .then(response => response.json())
  .then(data => {
    document.getElementById('user-info').innerText = `User: ${data.name}`;
  });

And here’s a naive Cypress test that waits for this data:

// cypress/e2e/user.cy.js
describe('User Data Test', () => {
  it('displays user name', () => {
    cy.visit('/'); // This makes a real request to /api/user
    cy.get('#user-info').should('contain', 'User:');
  });
});

When you run this, Cypress has to wait for the server to respond to /api/user. If the server is slow, or the network is flaky, your test will be slow and flaky too.

The core problem Cypress solves here is bridging the gap between your frontend tests and the backend dependencies they rely on. By default, it interacts with your application as a user would, including making actual HTTP requests. This makes tests realistic but brittle and slow.

The solution is stubbing and mocking. Instead of letting your frontend fetch call hit the actual network, you intercept it within Cypress and provide a canned response. This makes your tests lightning fast and perfectly reliable, as they no longer depend on external services.

Here’s how we replace the real network call with a stubbed one:

// cypress/e2e/user.cy.js (with stubbing)
describe('User Data Test', () => {
  it('displays user name with stubbed data', () => {
    // Intercept the GET request to /api/user
    cy.intercept('GET', '/api/user', {
      statusCode: 200,
      body: {
        name: 'Alice',
      },
    }).as('getUser'); // Give the request an alias

    cy.visit('/'); // This no longer waits for a real network response

    // Wait for the intercepted request to complete
    cy.wait('@getUser');

    // Assert that the stubbed data is displayed
    cy.get('#user-info').should('contain', 'User: Alice');
  });
});

When cy.visit('/') runs, Cypress sees that app.js will try to fetch('/api/user'). Because we’ve used cy.intercept('GET', '/api/user', ...) before the fetch happens, Cypress intercepts that request. It immediately returns the statusCode: 200 and body: { name: 'Alice' } that we defined, without ever touching the actual network. cy.wait('@getUser') then confirms that our stubbed request was indeed made and completed.

The mental model is that Cypress becomes a powerful proxy for your application’s network layer during testing. You tell it, "When you see a request for X, respond with Y." This gives you granular control over the data your frontend receives.

The exact levers you control are the HTTP method (GET, POST, etc.), the URL pattern (exact match, wildcards, regex), the response status code, and the response body. You can even simulate network errors or slow responses if you need to test those scenarios.

A common pitfall is placing cy.intercept() calls after the action that triggers the network request. For instance, if your fetch is called immediately on page load, and you put cy.intercept() after cy.visit(), the initial request will bypass your interceptor and hit the real network, causing your test to slow down or fail. Always declare your intercepts before the action that initiates the network call.

The next concept you’ll naturally run into is how to handle more complex scenarios, like multiple intercepted requests or dynamically generated request bodies.

Want structured learning?

Take the full Cypress course →