Cypress Network Stubbing with cy.intercept()
AI generated
PASS
expect()
Testing · Cypress · E2E Testing · Magento 2
Cypress Network Stubbing with cy.intercept()
Deterministic E2E tests instead of flaky ones

Testing Magento checkout flows only against the real API gives you slow, flaky end-to-end tests that turn red on every backend hiccup. With cy.intercept() you stub and observe network requests on purpose, load fixtures for realistic responses, and test error and loading states deterministically, without ever touching a real server. That keeps your tests fast, reproducible, and independent of the current database state.

14 min. read cy.intercept() · Fixtures · GraphQL Magento 2.4.8 · Hyvä Theme · Cypress 13

1. Why network stubbing is essential for E2E testing Magento stores

End-to-end tests that run exclusively against a real Magento backend are structurally unstable: stock levels change, prices get recalculated, third-party APIs for payment or shipping respond at different speeds, and every test environment drifts over time. A test that's green today can turn red tomorrow for no reason other than a product disappearing from the catalog. cy.intercept() solves this by intercepting network requests at the browser level before they reach the backend, returning controlled, reproducible responses instead.

This is deliberately distinct from plain PHPUnit unit testing: while unit tests check individual PHP classes in isolation, Cypress tests the whole system from the browser's point of view, including Alpine.js interactions, GraphQL calls, and the actual rendering logic in the Hyvä theme. Network stubbing is not a replacement for backend tests, but a targeted way to control the one variable that makes E2E tests unstable most often: the server's response. Once you control that variable, you can reliably force edge cases like empty carts, failed payments, or sold-out products without manually preparing the test database.

2. cy.intercept() fundamentals: syntax, matching, and aliasing

cy.intercept() comes in several signature variants: cy.intercept(url) for pure observation, cy.intercept(method, url, response) for simple stubbing, and cy.intercept(routeMatcher, handler) for full control over the request and response through a callback function. Two approaches are available for URL matching: glob patterns like '**/rest/V1/products*' are quick to write and sufficient for most REST endpoints, while RegExp objects like /\/rest\/V1\/products\/\d+$/ allow precise matching when several endpoints would otherwise overlap.

Every intercept should get a descriptive alias via .as('alias'). Without an alias, an intercepted request can neither be awaited nor referenced in assertions later on. Order matters too: Cypress evaluates multiple matching cy.intercept() definitions in reverse registration order, so the most recently registered pattern wins on overlap. That lets you set a global default stub for a test and override it selectively inside individual it() blocks, for example for a single error case.


// Basic GET intercept with fixture-based response mocking
cy.intercept('GET', '/rest/V1/products*', {
  fixture: 'products/list.json'
}).as('getProducts');

cy.visit('/catalogsearch/result/?q=hyva');
cy.wait('@getProducts');

// Glob pattern matches any query string after products
cy.intercept('GET', '**/rest/V1/products**').as('getProductsGlob');

// RegExp pattern for precise path matching, no accidental overlap
cy.intercept({
  method: 'GET',
  url: /\/rest\/V1\/products\/\d+$/
}).as('getSingleProduct');

3. Fixture-based response mocking with cypress/fixtures/*.json

Fixtures are static JSON files under cypress/fixtures/ that simulate realistic backend responses. Instead of defining a response inline in the test, you reference a file via { fixture: 'products/list.json' }, or load it explicitly with cy.fixture('products/list.json').then((data) => ...) when the data still needs to be modified before use. The advantage over inline objects: fixtures are version-controlled, reusable across multiple spec files, and can be named clearly, for example empty.json, out-of-stock.json, or 500-error.json for the respective edge cases.

A good fixture structure mirrors the actual shape of the Magento REST or GraphQL response, including every field the frontend accesses. Incomplete fixtures are a common source of bugs: if a field like extension_attributes, which the component actually expects, is missing, the test can stay falsely green even though the real backend returns a different shape. It's worth regularly diffing fixtures against a real API response, or generating them automatically from a staging system, rather than writing them once by hand and never updating them.


{
  "items": [
    {
      "id": 1042,
      "sku": "MS-HOODIE-BLK-M",
      "name": "Mironsoft Hoodie Black",
      "price": 59.9,
      "status": 1,
      "extension_attributes": {
        "stock_item": {
          "qty": 24,
          "is_in_stock": true
        }
      }
    },
    {
      "id": 1043,
      "sku": "MS-CAP-GRN",
      "name": "Mironsoft Cap Green",
      "price": 24.5,
      "status": 1,
      "extension_attributes": {
        "stock_item": {
          "qty": 0,
          "is_in_stock": false
        }
      }
    }
  ],
  "total_count": 2
}

4. Deterministic waiting: cy.wait('@alias') instead of fixed timeouts

A classic anti-pattern in E2E tests is cy.wait(2000): a fixed wait that's either too short and makes the test flaky, or too long and needlessly slows down the entire suite. cy.wait('@alias') solves this structurally by waiting exactly as long as it takes for the aliased request to actually complete, whether that's 50 milliseconds or 3 seconds. Only then does Cypress run the next command, ruling out race conditions between UI updates and network responses.

cy.wait() also accepts an array of aliases, for example cy.wait(['@getProducts', '@getCategories']), when a page fires several parallel requests and the test should only proceed once all of them have completed. On top of that, cy.wait('@alias') returns an interception object that you can use directly for further assertions, for example .its('response.statusCode').should('eq', 200). Worth noting: a timeout on cy.wait() is itself a valuable test result, it reliably shows that an expected request was never fired, which can point to a real frontend bug.

5. Testing error and loading states deterministically

Error states like a 500 from the server or a timeout are hard to reproduce reliably against a real backend, but trivial with cy.intercept(), a single line does the job. Using { statusCode: 500, body: {...} } you simulate a backend failure exactly when the test needs it, then check whether the UI shows the expected error message instead of simply hanging. For a complete network outage, forceNetworkError: true is the right tool, it simulates an aborted request rather than a regular error response.

For loading states, the delay option inside a req.reply() handler is key: it artificially delays the response, say by 3 seconds, so a test can specifically check that a loading spinner is visible during that window and correctly disappears afterward. Without this artificial delay, loading states are practically impossible to test reliably, since local test environments usually respond too fast for the intermediate state to ever become visible.


// Simulate a hard backend failure
cy.intercept('GET', '**/rest/V1/products*', {
  statusCode: 500,
  body: { message: 'Internal Server Error' }
}).as('getProductsError');

cy.visit('/catalog/category/view/id/5');
cy.wait('@getProductsError');
cy.get('[data-testid="error-banner"]').should('be.visible');

// Simulate a slow response to test loading spinners deterministically
cy.intercept('GET', '**/rest/V1/products*', (req) => {
  req.reply({ delay: 3000, fixture: 'products/list.json' });
}).as('getProductsSlow');

cy.get('[data-testid="loading-spinner"]').should('be.visible');
cy.wait('@getProductsSlow');
cy.get('[data-testid="loading-spinner"]').should('not.exist');

6. Asserting on request and response payloads

Network stubbing isn't just about faking responses, it's also about checking what the frontend actually sends to the backend. Inside a cy.intercept() callback handler, the incoming req is fully available, including req.body, req.headers, and req.url. That lets you assert directly in the handler, for example that a checkout request contains the correct payment method before any response is even returned: expect(req.body.paymentMethod.method).to.eq('checkmo').

Alternatively, the interception object returned by cy.wait('@alias') exposes the same request afterward for inspection, for example via cy.wait('@placeOrder').its('request.body.cartId').should('exist'). This deferred style works particularly well when several assertions should read cleanly one after another in the test chain. For more complex checks, such as verifying that a cart request contains exactly the expected line items in the right order, a combination of deep.equal and targeted property checks keeps failure messages meaningful when a test breaks.


// Intercept POST checkout request and assert on the outgoing payload
cy.intercept('POST', '**/rest/V1/carts/*/payment-information', (req) => {
  expect(req.body).to.have.property('paymentMethod');
  expect(req.body.paymentMethod.method).to.eq('checkmo');
  req.reply({ statusCode: 200, body: 12345 });
}).as('placeOrder');

cy.get('[data-testid="place-order"]').click();
cy.wait('@placeOrder').its('response.statusCode').should('eq', 200);

7. req.continue() and req.reply(): spying vs. stubbing in detail

A cy.intercept() without a defined response acts as a pure spy: the request goes to the real backend unchanged, Cypress just observes it and makes it assertable through cy.wait('@alias') and the interception object. That's useful for verifying that a tracking event or an analytics call actually fires, without altering its behavior. Inside a handler, the same behavior can be forced explicitly with req.continue(), optionally with a callback function that inspects or modifies the response once it arrives.

req.reply(), on the other hand, fully stubs the response and prevents the request from ever reaching the real backend. Both functions can be combined: req.continue((res) => { res.body.items = res.body.items.slice(0, 1); }) lets the request run for real but modifies the returning response, for instance to artificially trim a large product list for the test. This middle ground is particularly valuable for snapshot-style tests where the real backend logic should still apply, but the data volume in the test needs to stay controllable.

8. Aliasing Magento GraphQL requests by operation name

Magento's GraphQL API runs through a single endpoint, usually /graphql, which makes plain URL matching useless for cy.intercept(): every query and every mutation lands on the same route. The solution lives in the request body: every GraphQL request carries an operationName field that you can evaluate inside the callback handler to alias individual operations selectively. That lets a test wait precisely for ProductDetailQuery without accidentally reacting to a completely different query like MegaMenu that happens to fire around the same time.

Dynamically setting req.alias inside the handler is the most reliable approach here, since it still works when several GraphQL operations hit the same URL within a short window. Without this targeted aliasing, you're left with a generic cy.intercept('POST', '**/graphql') catch-all that intercepts everything but can't distinguish between individual operations, making tests imprecise and brittle, especially on pages with many parallel GraphQL calls like the Magento product detail page.


// Alias Magento GraphQL requests by operation name, not by URL
cy.intercept('POST', '**/graphql', (req) => {
  if (req.body.operationName === 'ProductDetail') {
    req.alias = 'productDetailQuery';
  }
  if (req.body.operationName === 'AddSimpleProductsToCart') {
    req.alias = 'addToCartMutation';
  }
});

cy.visit('/simple-product.html');
cy.wait('@productDetailQuery');

cy.get('[data-testid="add-to-cart"]').click();
cy.wait('@addToCartMutation').its('response.body.data').should('exist');

9. When to stub, when to hit the real backend: contract test tradeoffs

Network stubbing makes tests fast and deterministic, but it carries a real risk: false confidence. If a fixture no longer matches the actual API shape because the backend changed, the Cypress test can stay green even though the integration has long since broken in production. That's why critical paths like completing checkout or processing payment should run at least once, in a nightly suite, against a real staging backend, while UI states like errors, empty lists, or loading animations get stubbed deliberately.

A sensible addition is dedicated contract testing, for example with Pact or OpenAPI schema validation, run separately from the UI tests to ensure the real backend still matches the shape the fixtures assume. That keeps responsibilities cleanly separated: Cypress with cy.intercept() verifies frontend behavior given a response, contract tests verify that response still matches reality. The table below summarizes when each pattern is the better choice.

Scenario Naive pattern Risk Recommended pattern
Waiting on requests cy.wait(2000) with a fixed delay Flaky on a slow CI runner cy.wait('@alias') on an intercept
Checkout & payment Stub the payment gateway entirely A broken contract goes unnoticed Test the critical path against staging
Error states Not tested at all 500 handling silently breaks in prod Stub statusCode 500 deliberately
GraphQL queries Match by URL only Alias collisions, wrong interception Match on operationName
Empty result lists Only the happy path with data Empty state stays untested Stub a fixture with an empty array

In practice, a hybrid strategy works best: most of the suite runs fast and stubbed against fixtures, a lean set of smoke tests covers the business-critical paths against a real backend, and contract tests close the gap in between. That keeps the suite fast without sacrificing confidence in the flows that matter most.

Mironsoft

Cypress E2E testing and test automation for Magento and Hyvä stores

Want stable E2E tests for your Magento store?

We build resilient Cypress test suites for your Magento and Hyvä stores, from the first fixture strategy to full CI/CD integration, so your critical flows stay reliably green.

Cypress test setup

Intercepts, fixtures, and alias strategies built cleanly from the ground up

GraphQL test coverage

Operation-based aliasing for Magento's GraphQL API

CI/CD integration

Cypress wired into your pipeline, with stable, fast runs

10. Summary

cy.intercept() solves the core problem behind unstable E2E tests: dependence on a real, constantly changing backend. Fixture-based stubbing lets you reproducibly force loading states, error cases, and empty states, while cy.wait('@alias') structurally rules out race conditions between UI and network response, instead of merely papering over them with fixed timeouts. For Magento's GraphQL API, operation-based aliasing through the request body is the only reliable way to target individual queries and mutations, since URL matching fails against the single /graphql endpoint.

The decisive point isn't technical skill, though, it's strategy: stubbing is powerful, but only as trustworthy as the fixtures it's built on. Critical business processes like checkout and payment belong additionally in a suite that runs against a real backend, complemented by separate contract tests that guard the API shape. That keeps the test suite fast and meaningful at the same time, rather than just fast and blindly green.

Cypress Network Stubbing with cy.intercept() - The Essentials at a Glance

cy.intercept() fundamentals

Glob or RegExp matching, always tag with .as('alias'), later definitions override earlier ones.

Fixtures

JSON files under cypress/fixtures/, version-controlled and reusable for edge cases like empty states.

Deterministic waiting

cy.wait('@alias') instead of fixed timeouts, structurally prevents race conditions.

Stub vs. real backend

Test critical paths against staging too, guard against API drift with contract tests.

11. FAQ: Cypress Network Stubbing with cy.intercept()

1What is cy.intercept() and what is it used for in Cypress?
The Cypress API for intercepting, observing, and stubbing network requests in the browser. Replaces real backend responses with controlled, reproducible ones for faster, more deterministic E2E tests.
2How does stubbing differ from spying with cy.intercept()?
Spying observes the real request unchanged, stubbing replaces the response entirely via req.reply(). Combinable through req.continue() and req.reply() in the same handler.
3How do I load a fixture file into an intercept?
Inline with { fixture: 'products/list.json' } or explicitly via cy.fixture(...).then() when the data needs modifying beforehand.
4How do I reliably wait for an intercepted request?
With cy.wait('@alias'). Cypress waits exactly until the request completes, instead of letting a fixed duration pass with cy.wait(ms).
5How do I simulate a 500 error or a slow response?
Errors via { statusCode: 500, body: {...} }, slow responses via req.reply({ delay: 3000, fixture: '...' }) inside the handler.
6How do I test an empty result state?
Via a fixture with an empty items array, for example empty.json. Lets you check whether the UI shows an appropriate empty-state message instead of an error.
7How do I check the request body of an intercepted POST request?
Directly in the handler via req.body before req.reply(), or afterward via the interception object from cy.wait('@alias') using .its('request.body').
8How do I intercept Magento GraphQL requests by operation?
Evaluate req.body.operationName in the handler and set req.alias dynamically, since all operations go through the same /graphql endpoint.
9When should I NOT stub, and test against the real API instead?
For checkout and payment processing, since over-stubbing there creates false confidence. Test additionally in a nightly suite against real staging, complemented by contract tests.
10What is the difference between glob patterns and RegExp for URL matching?
Glob patterns are simple and usually sufficient, RegExp allows more precise matching, for example to deliberately avoid overlap with similar endpoints.