Deterministic API Mocking with Cypress cy.intercept(): Beyond Basic Stubbing
Move beyond arbitrary cy.wait() calls. Cypress cy.intercept() enables deterministic API mocking, latency simulation, and contract validation across e2e and component tests—with practical patterns for GraphQL, binary payloads, and early-request blind spots.
28 Jul 2025, 13:34 UTC

The flaky test problem nobody talks about
Your CI pipeline passes locally but fails on the build server. The culprit? A third-party payment API that occasionally returns 500ms responses, sometimes 3s, and once in a blue moon times out entirely. Your test suite uses cy.wait(2000) as a band-aid. It works until it doesn't.
Cypress's cy.intercept() replaces the legacy cy.server()/cy.route() API with a more powerful model: match requests by URL glob, RegExp, predicate function, or Minimatch pattern, then stub, modify, or pass them through. The same interception logic works identically in end-to-end and component testing modes, so your mock strategy scales across the test pyramid.
Matching granularity that prevents handler collisions
Intercepted requests execute handlers in registration order. Overlapping patterns cause later handlers to unintentionally override earlier ones—a common source of "why is my stub ignored?" debugging sessions.
Use predicate functions for precise control:
cy.intercept({
method: 'POST',
url: '/graphql',
// Match only the mutation we care about
predicate: (req) => req.body.operationName === 'UpdateUserProfile'
}, {
statusCode: 200,
body: { data: { updateUserProfile: { success: true } } }
}).as('updateProfile')
This avoids stubbing unrelated GraphQL operations that share the same /graphql endpoint.
Worked example: Simulating latency and validating contracts
Suppose a dashboard component fetches user metrics from /api/v1/metrics and implements exponential backoff on 5xx responses. You want to verify the retry logic without a real backend.
- Define a fixture at
cypress/fixtures/metrics.jsonwith realistic payload structure. - Intercept with dynamic delay to simulate network jitter:
cy.intercept('GET', '/api/v1/metrics', (req) => {
// Simulate 100-500ms variable latency
const delay = Math.floor(Math.random() * 400) + 100;
req.reply((res) => {
res.setDelay(delay);
res.send({ fixture: 'metrics.json' });
});
}).as('fetchMetrics');
- Trigger the fetch and wait deterministically:
cy.visit('/dashboard');
cy.wait('@fetchMetrics').its('response.statusCode').should('eq', 200);
- Assert on the outgoing request (contract validation):
cy.wait('@fetchMetrics').then((interception) => {
expect(interception.request.headers).to.have.property('authorization');
expect(interception.request.query).to.include.keys('period', 'tz');
});
Run with npx cypress run --spec "cypress/e2e/dashboard.cy.js" in a headed or headless CI runner. The test execution time now reflects actual app behavior, not arbitrary sleeps.
Trade-offs you'll hit in practice
- Initial navigation blind spot:
cy.intercept()cannot capture requests made during the initial HTML load before Cypress injects its proxy. For early-request mocking, usecy.visit(url, { onBeforeLoad(win) { /* stub via win.fetch */ } }). - Memory pressure with large binaries: File downloads or video streams buffered for modification can OOM headed CI runners. Prefer passthrough for heavy payloads:
cy.intercept('/assets/**', { passthrough: true }). - GraphQL predicate complexity: Matching on
operationNameor variables inside the JSON body adds boilerplate. Consider a helper:const gqlOp = (name) => (req) => req.body?.operationName === name.
Verify it works in your repo today
Create a minimal reproduction:
npm init -y
npm i -D cypress@latest
# Add a test file with the intercept pattern above
npx cypress run
Check the Cypress Real World App (github.com/cypress-io/cypress-realworld-app) for production-grade patterns in cypress/e2e and cypress/component. Consult the official docs at docs.cypress.io/api/commands/intercept for the current signature and migration guide from cy.route().
If your test suite still sprinkles cy.wait(ms) as synchronization, replace one with an alias-based wait this sprint. The flake rate drops, and the test duration becomes a meaningful signal again.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.