Resource library

QA How-To

How to Use Cypress network stubbing (2026)

Learn Cypress network stubbing with cy.intercept, fixtures, dynamic handlers, errors, GraphQL, type-safe assertions, debugging, and a practical test strategy.

22 min read | 2,800 words

TL;DR

Cypress network stubbing uses cy.intercept() to replace matching browser responses with controlled data. Define the route before the trigger, alias it, exercise the UI, wait for the alias, and assert the request plus the visible result.

Key Takeaways

  • Register cy.intercept before the application can issue the matching browser request.
  • Choose deliberately between spying on the real response, returning a StaticResponse, and using a dynamic route handler.
  • Match method, pathname, query, and stable headers narrowly enough to prevent false synchronization.
  • Treat stubbed UI tests as frontend contract tests, then retain non-stubbed and API coverage for service behavior.
  • Model HTTP errors, network failures, loading, retries, and malformed data as separate risk scenarios.
  • Keep fixtures synthetic, typed, minimal, and governed by the same response contract as the service.

Cypress network stubbing lets a test control the response to a browser request with cy.intercept(). Register a precise route before the request is sent, return a static or dynamic response, alias the route, trigger the behavior, and assert both the network exchange and the user-visible outcome.

The technique is valuable for empty states, rare failures, loading transitions, permissions, pagination, and other UI conditions that are difficult to create reliably through a live service. It also changes the test boundary. A stub proves how the browser handles the supplied response, not whether the real backend produced that response correctly.

TL;DR

it('renders a controlled inventory response', () => {
  cy.intercept('GET', '/api/inventory', {
    statusCode: 200,
    body: {
      items: [{ sku: 'QA-101', name: 'Test Notebook', stock: 7 }],
    },
  }).as('getInventory')

  cy.visit('/inventory')
  cy.wait('@getInventory').its('response.statusCode').should('eq', 200)
  cy.contains('Test Notebook').should('be.visible')
})
Technique Server reached? Primary use Main caution
Spy-only intercept Yes Synchronize and inspect real traffic Environment remains variable
Static response No Deterministic UI state Does not validate backend
req.reply() Usually no Conditional or computed stub Handler complexity can hide intent
req.continue() Yes Inspect or modify real response Mutation can mask integration defects
forceNetworkError No successful exchange Offline and transport failure Different from an HTTP error

1. What Cypress Network Stubbing Actually Controls

cy.intercept() hooks matching requests made by the application under test. With no response handler it spies on traffic and allows the request to continue. With a StaticResponse it answers without waiting for the destination server. With a route handler it can inspect or modify the outgoing request, reply dynamically, let the request continue, and inspect the real response.

That distinction creates three useful test boundaries. A spy-only browser test checks a live integration while using the request as a synchronization point. A stubbed browser test checks frontend behavior against controlled contract data. A pass-through modification test reaches the service but changes part of the exchange. Label these honestly in suite documentation and reporting.

Cypress network interception is not a general proxy for every HTTP call in the test process. cy.request() sends HTTP directly and bypasses routes registered with cy.intercept(). Calls executed by Node tasks also sit outside the browser traffic controlled by the intercept. This matters when a test uses API setup and then expects a browser alias to observe it.

All intercepts are cleared before each test, which supports isolation. Register common baseline routes in beforeEach, then define scenario-specific overrides inside the test. Normal intercepts are evaluated in reverse order of definition, making later overrides possible. Routes with { middleware: true } run first and in definition order, so use them intentionally.

2. Build the Smallest Correct Stub

A useful network stub has four parts: a narrow matcher, a plausible response, an alias, and a UI assertion. The order is important because a page can send an initial request immediately during cy.visit().

describe('inventory page', () => {
  it('shows the empty state', () => {
    cy.intercept('GET', '/api/inventory', {
      statusCode: 200,
      headers: { 'content-type': 'application/json' },
      body: { items: [], nextCursor: null },
    }).as('getInventory')

    cy.visit('/inventory')

    cy.wait('@getInventory').then(({ request, response }) => {
      expect(request.method).to.eq('GET')
      expect(response?.statusCode).to.eq(200)
    })

    cy.contains('No inventory items').should('be.visible')
    cy.contains('button', 'Add item').should('be.enabled')
  })
})

The network assertion confirms that the intended route matched. The UI assertions confirm that the application consumed the response correctly. An assertion on the alias alone is incomplete if the purpose is user behavior, and a UI assertion alone can pass using cached or default data.

Use a realistic status code and response shape. A successful create operation may return 201, while a delete can return 204 with no body. Do not normalize every response to 200 merely because the frontend accepts it. The stub becomes executable documentation, so inaccurate status and header behavior can train the suite to accept a contract the service never promises.

Keep the initial example inline. Move data to a fixture or helper only when reuse or readability improves enough to justify navigation away from the scenario.

3. Match Requests Without Catching the Wrong Call

Cypress accepts URL strings, glob patterns, regular expressions, and RouteMatcher objects. A RouteMatcher can constrain method, url, hostname, path, pathname, query, headers, https, port, auth, times, and middleware. Every populated field must match.

cy.intercept({
  method: 'GET',
  pathname: '/api/inventory/search',
  query: {
    q: 'notebook',
    page: '1',
  },
}).as('searchInventory')

Using pathname and query separately is often clearer than embedding an ordered query string in the URL. Browser clients may reorder or encode parameters. Match only headers that belong to the scenario, because volatile correlation and tracing headers make routes unnecessarily fragile.

Glob strings use Cypress minimatch behavior. A pattern such as **/api/items/* is useful across test hosts, while a regular expression can express a numeric identifier precisely:

cy.intercept('GET', /\/api\/items\/\d+$/).as('getItem')

Debug ambiguous globs with Cypress.minimatch() in a focused assertion. Avoid **/* as a timeout fix. It can allow an analytics call or favicon request to satisfy an alias intended for business data.

Specify the HTTP method for endpoints that share a path. A broad /api/items intercept can accidentally stub a POST that the test meant to let through. Route names should describe business operations, such as getInventory or createItem, instead of generic names like apiCall.

4. Design StaticResponse Objects That Reflect the Contract

A Cypress StaticResponse can define statusCode, headers, body, fixture, forceNetworkError, delay, throttleKbps, and log. body and fixture are alternatives. Use the smallest set of fields that makes the scenario accurate.

cy.intercept('GET', '/api/inventory/QA-101', {
  statusCode: 200,
  headers: {
    'content-type': 'application/json',
    'cache-control': 'no-store',
  },
  body: {
    sku: 'QA-101',
    name: 'Test Notebook',
    stock: 7,
    warehouse: { id: 'west', label: 'West warehouse' },
  },
}).as('getInventoryItem')

Do not copy an enormous production response when the view reads five properties. Extra fields obscure which contract matters and make updates noisy. On the other hand, avoid a response so small that it bypasses realistic parsing, optional fields, or nesting. Define a builder with sensible defaults when many tests vary one field.

Headers can be essential. Content type affects parsing, cache headers affect whether a later request reaches the network, and location headers can drive redirects. Include them when the application behavior depends on them.

delay adds a minimum latency in milliseconds, while throttleKbps limits transfer rate in kilobits per second. These controls are good for exposing loading and progressive states. They do not measure production performance. Keep the delay short and explain its purpose so the test is not mistaken for a benchmark.

Set { log: false } only for noisy unimportant traffic. Suppressing business requests removes useful evidence from the Command Log and can make CI failures harder to diagnose.

5. Use Fixtures and Typed Builders Responsibly

Fixtures work well for larger reusable payloads. Place synthetic JSON under cypress/fixtures and serve it directly:

cy.intercept('GET', '/api/inventory', {
  fixture: 'inventory/available-items.json',
}).as('getInventory')

When a test needs to change one field, load the fixture before registering the route:

cy.fixture('inventory/available-items.json').then((inventory) => {
  cy.intercept('GET', '/api/inventory', {
    statusCode: 200,
    body: {
      ...inventory,
      items: inventory.items.map((item: { sku: string; stock: number }) => ({
        ...item,
        stock: item.sku === 'QA-101' ? 0 : item.stock,
      })),
    },
  }).as('getInventory')
})

cy.visit('/inventory')

The command queue preserves order, so the visit occurs after fixture loading and route registration. Do not call cy.fixture() from inside the intercept handler. Network handlers execute during the request lifecycle, and Cypress commands should not be enqueued there.

Typed object builders are often easier than a folder full of near-identical snapshots:

type Item = { sku: string; name: string; stock: number }

const buildItem = (overrides: Partial<Item> = {}): Item => ({
  sku: 'QA-101',
  name: 'Test Notebook',
  stock: 7,
  ...overrides,
})

Builders make the changed property visible in each scenario. Fixtures remain valuable for document-sized or deeply nested bodies. For fixture loading details, see Cypress cy.fixture examples.

6. Return Dynamic Responses with req.reply

A route handler receives the intercepted request and can select a response at request time. Call req.reply() to end the request phase with a supplied body or StaticResponse.

cy.intercept('POST', '/api/inventory/reservations', (req) => {
  const { quantity } = req.body as { sku: string; quantity: number }

  if (quantity > 5) {
    req.reply({
      statusCode: 409,
      body: {
        code: 'INSUFFICIENT_STOCK',
        message: 'Only five units are available',
      },
    })
    return
  }

  req.reply({
    statusCode: 201,
    body: { reservationId: 'res-101', status: 'held' },
  })
}).as('reserveInventory')

This pattern tests conditional UI behavior through the same endpoint. Keep it deterministic. If the handler grows into a fake service with mutable databases, authentication rules, and broad routing, maintenance cost rises quickly and the fake can diverge from production.

You can modify outgoing request properties before allowing the call to continue. For example, a controlled header can activate a server-side test feature. If the handler neither replies nor explicitly continues, Cypress applies request changes and proceeds through any remaining matching handlers before reaching the destination.

Use req.destroy() or a static { forceNetworkError: true } for a transport failure. req.redirect(location, statusCode?) creates a redirect. These are real current request-handler APIs, but each should represent a product behavior the client is expected to handle.

Name dynamic helpers after the scenario, such as stubReservationConflict, rather than exposing every low-level response knob to test authors.

7. Inspect Real Responses with req.continue

req.continue() sends the request to the upstream server. An optional callback receives the completed response before it reaches the browser. The callback can assert or modify statusCode, headers, and body.

cy.intercept('GET', '/api/inventory', (req) => {
  req.continue((res) => {
    expect(res.statusCode).to.eq(200)
    expect(res.body).to.have.property('items')
    res.headers['cache-control'] = 'no-store'
  })
}).as('getRealInventory')

Only one matching handler should call req.continue() for a request. Calling it ends the request phase and prevents propagation to later request handlers. Response event listeners registered through req.on('before:response'), req.on('response'), and req.on('after:response') offer more advanced lifecycle control.

Current response helpers include res.send(), res.setDelay(milliseconds), and res.setThrottle(kilobitsPerSecond). Use response mutation sparingly. If a pass-through test fixes missing fields or converts every error into success, it no longer reflects the deployed integration.

An assertion inside req.continue() can fail the test, but keep the user-facing assertion after cy.wait() too. Network shape and rendered behavior answer different questions. If the API contract needs comprehensive schema validation, prefer a dedicated API or contract test rather than expanding one browser route callback into a full validation suite.

8. Model Failure Modes Separately

An HTTP error is a completed HTTP response. A network error has no successful response. A timeout or delayed response is different again. Client logic often handles these through separate code paths, so one generic error test is insufficient.

it('shows forbidden access', () => {
  cy.intercept('GET', '/api/inventory', {
    statusCode: 403,
    body: { code: 'FORBIDDEN', message: 'Inventory access denied' },
  }).as('getInventory')

  cy.visit('/inventory')
  cy.wait('@getInventory')
  cy.contains('[role=alert]', 'You do not have inventory access')
    .should('be.visible')
})

it('offers retry after a network failure', () => {
  cy.intercept('GET', '/api/inventory', {
    forceNetworkError: true,
  }).as('failedInventory')

  cy.visit('/inventory')
  cy.wait('@failedInventory').should('have.property', 'error')
  cy.contains('button', 'Try again').should('be.visible')
})

Other valuable cases include 401 refresh behavior, 404 missing records, 409 business conflicts, 422 validation details, 429 rate limiting, 500 generic recovery, malformed JSON or schema drift, an empty success body, and a slow response. Select cases from the application's error contract rather than mechanically testing every status code in every view.

For retry behavior, fail the first call once with { times: 1 }, then allow a success route to answer the next request. Make assertions about the visible retry attempt and the number of matched calls. Never use uncontrolled random failure inside a test.

9. Stub Sequential Calls, Pagination, and Polling

Repeated requests need explicit state. The simplest retry uses a one-time route plus a fallback. Because normal routes are matched in reverse registration order, register the fallback first and the one-time override last.

cy.intercept('GET', '/api/inventory', {
  statusCode: 200,
  body: { items: [{ sku: 'QA-101', name: 'Recovered item' }] },
}).as('inventorySuccess')

cy.intercept(
  { method: 'GET', url: '/api/inventory', times: 1 },
  { statusCode: 503, body: { code: 'TEMPORARILY_UNAVAILABLE' } },
).as('inventoryFailure')

For cursor pagination, match query values and give each page an alias:

cy.intercept({
  method: 'GET',
  pathname: '/api/inventory',
  query: { cursor: 'page-2' },
}, {
  body: {
    items: [{ sku: 'QA-202', name: 'Automation Pencil' }],
    nextCursor: null,
  },
}).as('getSecondPage')

Register a separate first-page route with the correct initial query contract. Avoid one mutable counter when the request itself contains a stable page or cursor identifier. If a counter is unavoidable for polling, scope and reset it inside beforeEach, and make the expected sequence obvious.

Aliases expose all matched requests through @alias.all and individual matches through numbered aliases such as @alias.1. These are helpful for proving polling stops after success. Do not assert an exact poll count if the application contract only promises eventual completion within a time window, because rendering and scheduling can change the count.

10. Cypress Network Stubbing for GraphQL

GraphQL clients often send all queries and mutations to one URL. Match the POST endpoint, then inspect req.body.operationName to identify the operation. Assign a request-specific alias so tests can wait for the business operation rather than the generic transport route.

beforeEach(() => {
  cy.intercept('POST', '/graphql', (req) => {
    if (req.body.operationName === 'InventoryList') {
      req.alias = 'gqlInventoryList'
      req.reply({
        body: {
          data: {
            inventory: {
              nodes: [{ sku: 'QA-101', name: 'Test Notebook', stock: 7 }],
            },
          },
        },
      })
    }
  })
})

it('renders GraphQL inventory data', () => {
  cy.visit('/inventory')
  cy.wait('@gqlInventoryList').then(({ request, response }) => {
    expect(request.body.variables).to.deep.equal({ warehouseId: 'west' })
    expect(response?.body.errors).to.be.undefined
  })
  cy.contains('Test Notebook').should('be.visible')
})

GraphQL application errors commonly arrive with HTTP 200 and an errors array. Test the envelope, partial data policy, and client behavior. A transport-level 500 exercises a different branch.

Use stable operation names whenever the client sends them. Persisted queries may send a hash instead of readable query text, so match the identifier the actual client contract provides. Avoid formatting-sensitive string checks over the query document.

When several operations share one intercept handler, reply only for the target operation and let others pass. A broad GraphQL stub that silently returns the wrong operation shape creates confusing failures far from the route setup.

11. Keep Stubs Aligned with Service Contracts

Stub drift occurs when frontend fixtures keep passing after the backend changes a field, status, or error shape. Prevent it through shared schemas, generated types, contract fixtures, or validation in CI. The exact mechanism can be OpenAPI, GraphQL schema types, consumer-driven contracts, or a project-owned schema library.

TypeScript improves authoring but does not validate runtime JSON. Generic parameters on cy.intercept<RequestBody, ResponseBody>() help callbacks and assertions, yet a type assertion can still lie about an object literal loaded from disk. Runtime contract checks belong in a dedicated layer or a validated fixture-generation step.

type InventoryResponse = {
  items: Array<{ sku: string; name: string; stock: number }>
  nextCursor: string | null
}

cy.intercept<never, InventoryResponse>('GET', '/api/inventory', {
  statusCode: 200,
  body: { items: [], nextCursor: null },
}).as('getInventory')

Review fixture changes like production interface changes. A large snapshot replacement can hide a breaking rename. Prefer scenario-focused payloads and describe why a field differs. Do not capture production responses containing personal or confidential data.

Maintain at least one non-stubbed critical journey for each important integration, plus direct API tests for response contracts. The API error handling and negative testing guide shows how to cover service behavior that a browser stub intentionally excludes.

12. Debug Routes That Match Too Early, Too Late, or Never

When an alias times out, first inspect ordering. The intercept must be registered before cy.visit(), a click, form submission, timer, or application initialization sends the call. Next compare the actual request in the Command Log and browser developer tools with the method and route matcher.

Browser caching can prevent a network request, so an intercept cannot see it. Configure the test environment to disable API caching or use a carefully scoped middleware route to remove response cache headers:

beforeEach(() => {
  cy.intercept(
    { url: '**/api/**', middleware: true },
    (req) => {
      req.on('before:response', (res) => {
        res.headers['cache-control'] = 'no-store'
      })
    },
  )
})

Service workers can also fulfill requests along a path the test does not expect. Diagnose them through browser developer tools and decide whether the test environment should unregister the worker. Cross-origin host differences, missing leading slashes, base URL changes, and query encoding are other frequent causes.

If the wrong route answers, inspect overlapping intercept order. Later normal handlers match first, while middleware routes run first in definition order. Give scenario overrides distinct aliases so the test proves which one responded.

Finally, check application timing. An assertion that waits on a route the page only sends after an intersection observer fires may need the actual user action, such as scrolling the element into view. Network stubbing should synchronize with real product behavior, not replace it.

Interview Questions and Answers

Q: What is Cypress network stubbing?

It is the use of cy.intercept() to provide controlled responses for matching browser requests. I register the route before the trigger, alias it, exercise the UI, and assert the network exchange plus visible behavior. The result validates the frontend against the supplied contract.

Q: How is a spy different from a stub?

A spy-only intercept observes a request and allows the real server response. A stub supplies or changes the response. The spy preserves integration coverage, while the stub improves determinism and access to rare UI states.

Q: When would you use req.reply()?

I use it when the response depends on request body, headers, or other runtime data. It ends the request phase with a stub response. For a fixed response, an inline StaticResponse is usually simpler.

Q: When would you use req.continue()?

I use it to send the request upstream and optionally inspect or modify the real response. I avoid broad mutation because it can conceal a service defect. Only one matching request handler should call it.

Q: Why can an intercept miss a request?

It may be registered too late, use the wrong method or URL, lose to an overlapping route, or see no network call because browser cache or a service worker handled it. I compare the real request and route lifecycle before changing timeouts.

Q: How do you prevent fixture drift?

I derive or validate payloads against the owned API contract, use generated types where helpful, and retain direct API plus non-stubbed integration tests. Fixture review focuses on meaningful interface changes, not bulk snapshots.

Q: Does cy.request() pass through cy.intercept()?

No. cy.request() sends a direct request outside the application's browser traffic and bypasses registered intercept routes. I use it for setup or service checks, not to test a browser stub.

Common Mistakes

  • Registering the intercept after the request has already left the browser.
  • Omitting the method and accidentally stubbing another operation on the same path.
  • Using a broad glob that lets unrelated background traffic satisfy an alias.
  • Asserting only the network response and never the resulting UI state.
  • Calling a stubbed path an end-to-end validation of the real backend.
  • Treating 500, 429, network failure, timeout, and malformed data as one error.
  • Calling Cypress commands from inside an intercept route handler.
  • Copying sensitive production payloads into fixtures.
  • Growing route handlers into an undocumented fake backend.
  • Mutating every real response through req.continue() and masking integration defects.

Conclusion

Cypress network stubbing is most effective when each route has a clear test boundary. Match the intended browser call precisely, supply contract-accurate data, wait on the alias, and verify what the user sees. Use static responses for clarity and dynamic handlers only when the request genuinely determines the scenario.

Start with one unstable or hard-to-create UI state, replace its external dependency with a focused stub, and retain a non-stubbed path for integration evidence. For a larger recipe collection, continue with Cypress network stubbing examples.

Interview Questions and Answers

Explain the three main cy.intercept modes.

A route without a handler spies and lets the real request continue. A StaticResponse stubs the response immediately. A route function can inspect or modify the request, reply dynamically, or continue to and inspect the real server.

What does Cypress network stubbing prove?

It proves how the browser application behaves for the response the test supplies. It does not prove that the deployed backend returns that response. I pair it with API contract coverage and selected non-stubbed journeys.

How do you match query parameters reliably?

I use a RouteMatcher with pathname and query fields instead of embedding an order-sensitive query string. I match only values relevant to the scenario and inspect the actual encoded request when debugging.

How are overlapping intercepts ordered?

Normal routes are evaluated in reverse definition order, which allows a later scenario route to override a baseline. Routes with middleware true run first and in definition order. I give overrides distinct aliases to make the selected route observable.

How would you test one failed request followed by success?

I register a success fallback first and then a failure route with times set to one. The later normal route answers the first call and expires, so the fallback answers the retry. I assert both aliases and the visible recovery.

Why should Cypress commands not run inside an intercept handler?

The route handler executes during the network lifecycle, outside the normal place for queuing Cypress commands. I load fixtures or prepare values before registering the route, then keep the handler synchronous or return its supported asynchronous work.

How do you distinguish an HTTP error from a network error?

An HTTP error has a completed response with a status such as 403 or 500. A network error destroys or fails the connection and may yield an interception error instead of a response. Client recovery often differs, so I test both explicitly.

How do you keep stubs contract-accurate?

I keep payloads small and synthetic, validate them against an owned schema or generated types, and review fixture changes as interface changes. Direct service tests and a few real browser integrations detect drift that TypeScript alone cannot.

Frequently Asked Questions

How do I stub an API response in Cypress?

Call cy.intercept with a method, URL matcher, and StaticResponse before the browser sends the request. Alias the route, trigger the UI action, wait for the alias, and assert the rendered result.

What is the difference between body and fixture in a StaticResponse?

body supplies the response value inline, while fixture loads a file from the fixtures directory. Use one or the other, and prefer inline data when the scenario is small enough to read easily.

Can Cypress stub GraphQL requests?

Yes. Intercept the GraphQL endpoint, inspect request.body.operationName or the persisted query identifier, and reply only for the target operation. Assert GraphQL errors separately from HTTP transport errors.

Why does cy.wait on my intercept time out?

The route may be registered after the request, the matcher may not match the actual method or URL, or cache or a service worker may prevent a network call. Overlapping intercept order can also cause a different route to answer.

Should every Cypress test stub the backend?

No. Stub detailed frontend states that need determinism, but retain non-stubbed critical journeys and direct API or contract tests. The mix should make each layer's evidence explicit.

How do I simulate a network failure in Cypress?

Return a StaticResponse with forceNetworkError set to true, or call req.destroy() from a route handler. This represents a transport failure and should be tested separately from an HTTP 500 response.

Can I delay a stubbed Cypress response?

Yes. Add delay in milliseconds to a StaticResponse, or call res.setDelay() in a response handler. Use it to expose loading behavior, not as a performance measurement.

Related Guides