QA How-To
Cypress network stubbing: Examples
Use each Cypress network stubbing example for success, errors, fixtures, requests, GraphQL, pagination, retries, loading states, and contract-focused UI tests.
23 min read | 2,192 words
TL;DR
A reusable Cypress network stubbing example registers cy.intercept() before the trigger, supplies a contract-accurate response, aliases the route, and asserts the request plus UI. The recipes below cover static, dynamic, sequential, GraphQL, file, error, and typed cases.
Key Takeaways
- Put the intercept before visit, reload, click, or any other action that can trigger the request.
- Alias each business operation and verify both the intercepted exchange and the rendered behavior.
- Prefer RouteMatcher objects for query-sensitive routes and operation names for GraphQL requests.
- Use times for one-shot failure behavior and explicit state for longer response sequences.
- Keep request handlers deterministic and prepare fixture data before the handler runs.
- Select the smallest recipe that expresses the scenario, then preserve separate real integration coverage.
A Cypress network stubbing example is useful only when it shows the full test flow: define cy.intercept() before the request, trigger the application behavior, wait on a meaningful alias, inspect the exchange, and verify the UI. The recipes in this guide are complete patterns you can adapt to your own routes and data contracts.
All examples use current Cypress interception APIs. They deliberately separate a stubbed frontend check from a real integration check. When a response is stubbed, the browser behavior is under test, while the backend response must be covered elsewhere.
TL;DR
cy.intercept('GET', '/api/profile', {
statusCode: 200,
body: { id: 'user-7', name: 'Morgan Tester' },
}).as('getProfile')
cy.visit('/profile')
cy.wait('@getProfile').its('response.statusCode').should('eq', 200)
cy.contains('Morgan Tester').should('be.visible')
| Need | Recommended recipe | Core API |
|---|---|---|
| Stable success state | Inline static body | StaticResponse.body |
| Large reusable data | Fixture response | StaticResponse.fixture |
| Response based on request | Dynamic handler | req.reply() |
| Assert a real service response | Pass-through handler | req.continue() |
| Retry recovery | One-shot route plus fallback | RouteMatcher.times |
| Transport failure | Destroy connection | forceNetworkError |
| Many GraphQL operations | Per-request alias | req.alias |
1. Minimal Cypress Network Stubbing Example
Start with an inline success response. It keeps the route, data, and assertions visible in one test, which makes intent easy to review.
describe('customer profile', () => {
it('renders the profile returned by the API', () => {
cy.intercept('GET', '/api/profile', {
statusCode: 200,
headers: { 'content-type': 'application/json' },
body: {
id: 'user-7',
name: 'Morgan Tester',
plan: 'team',
},
}).as('getProfile')
cy.visit('/profile')
cy.wait('@getProfile').then(({ request, response }) => {
expect(request.method).to.eq('GET')
expect(response?.statusCode).to.eq(200)
expect(response?.body.plan).to.eq('team')
})
cy.contains('h1', 'Morgan Tester').should('be.visible')
cy.contains('Team plan').should('be.visible')
})
})
Register before cy.visit() because the page can request the profile during initialization. The alias proves the intended call occurred, while the heading and plan assertions prove the view consumed the body.
Use this style when the payload is short and specific to one scenario. It is often clearer than a fixture whose only purpose is hiding six lines of JSON. Add only headers that affect application behavior. If the server's contract returns another status, such as 204 or 206, model that real contract instead of defaulting every stub to 200.
2. RouteMatcher Example with Path and Query
Search, sorting, and pagination requests should be matched on the inputs that drive the scenario. A structured RouteMatcher avoids coupling to query parameter order.
it('requests the second page sorted by newest', () => {
cy.intercept({
method: 'GET',
pathname: '/api/articles',
query: {
page: '2',
sort: 'newest',
},
}, {
statusCode: 200,
body: {
items: [
{ id: 'a-21', title: 'Testing retry behavior' },
{ id: 'a-22', title: 'Contract fixtures in CI' },
],
page: 2,
totalPages: 4,
},
}).as('getArticlePage')
cy.visit('/articles?page=1')
cy.contains('button', 'Next').click()
cy.wait('@getArticlePage').then(({ request }) => {
expect(request.query).to.include({ page: '2', sort: 'newest' })
})
cy.contains('Testing retry behavior').should('be.visible')
})
Every defined matcher property must match. Add hostname for a specific API host or headers only when a stable header is part of the behavior. Avoid matching trace IDs, random nonces, or other values generated independently on every request.
If the application uses a cursor, match the cursor rather than pretending it has page numbers. The stub should preserve request semantics. For broad host-independent patterns, glob URLs such as **/api/articles/* are available, and regex is useful for numeric identifiers. The Cypress cy.intercept example cookbook provides more matcher variations.
3. Fixture Response with a Scenario Override
Use a fixture for a large stable payload, then load it before route registration when the test changes one property.
type AccountFixture = {
id: string
name: string
limits: { projects: number; members: number }
}
it('shows the project limit warning', () => {
cy.fixture<AccountFixture>('accounts/team-account.json').then((account) => {
cy.intercept('GET', '/api/account', {
statusCode: 200,
body: {
...account,
limits: { ...account.limits, projects: 0 },
},
}).as('getAccount')
})
cy.visit('/projects/new')
cy.wait('@getAccount')
cy.contains('[role=alert]', 'Your project limit has been reached')
.should('be.visible')
})
Cypress queues the visit after the fixture callback and the intercept created inside it. Do not start cy.visit() before that chain. Also do not call cy.fixture() from an intercept route handler, because Cypress commands should not be enqueued during the network callback.
If the fixture needs no modification, use { fixture: 'accounts/team-account.json' } directly. Keep fixture files synthetic and name them after business scenarios. Avoid dated copies such as response-final-v3.json, which provide no clue about their purpose.
Validate fixtures against an API schema in a separate contract step. TypeScript typing improves editor feedback but does not prove that a JSON file conforms at runtime.
4. POST Request Assertion and Stubbed Creation
A create flow should check the payload the UI owns, return a plausible created resource, and assert the visible success transition.
type CreateProjectRequest = {
name: string
visibility: 'private' | 'team'
}
type CreateProjectResponse = {
id: string
name: string
visibility: 'private' | 'team'
}
it('creates a team project', () => {
cy.intercept<CreateProjectRequest, CreateProjectResponse>(
'POST',
'/api/projects',
{
statusCode: 201,
headers: { location: '/api/projects/project-42' },
body: {
id: 'project-42',
name: 'Release confidence',
visibility: 'team',
},
},
).as('createProject')
cy.visit('/projects/new')
cy.get('input[name=name]').type('Release confidence')
cy.get('select[name=visibility]').select('team')
cy.contains('button', 'Create project').click()
cy.wait('@createProject').then(({ request, response }) => {
expect(request.body).to.deep.equal({
name: 'Release confidence',
visibility: 'team',
})
expect(response?.statusCode).to.eq(201)
})
cy.location('pathname').should('eq', '/projects/project-42')
cy.contains('Project created').should('be.visible')
})
The generic types improve callback inference. They do not perform runtime schema validation. Assert fields controlled by the form and avoid repeating every generated server property in a browser test.
Returning Location demonstrates a response header that can influence navigation. Include it only if the client reads it. A contract-accurate stub is better than a feature-rich but imaginary response.
5. Dynamic req.reply Example Based on Request Body
Use a route handler when one endpoint should return different outcomes based on the outgoing request. The handler can inspect req.body, req.headers, req.query, req.url, and req.method.
it('shows a duplicate-name conflict', () => {
cy.intercept('POST', '/api/projects', (req) => {
const body = req.body as { name: string }
if (body.name === 'Existing project') {
req.reply({
statusCode: 409,
body: {
code: 'PROJECT_NAME_EXISTS',
field: 'name',
message: 'A project with this name already exists',
},
})
return
}
req.reply({
statusCode: 201,
body: { id: 'project-new', name: body.name },
})
}).as('createProject')
cy.visit('/projects/new')
cy.get('input[name=name]').type('Existing project')
cy.contains('button', 'Create project').click()
cy.wait('@createProject').its('response.statusCode').should('eq', 409)
cy.get('input[name=name]')
.should('have.attr', 'aria-invalid', 'true')
cy.contains('A project with this name already exists').should('be.visible')
})
The example returns on every path, which makes handler behavior obvious. Avoid random branches. A deterministic test must produce the same outcome from the same request.
If the handler only modifies the outgoing request and does not call reply or continue, the request proceeds through remaining handlers and eventually to the server. Use that for an explicit test header, not to silently rewrite normal product requests in every spec.
6. HTTP Error Table Tests
Several error statuses can drive the same component through different messages and actions. Define case data outside the test and create one isolated test per case.
const cases = [
{
statusCode: 401,
body: { code: 'AUTH_REQUIRED' },
expected: 'Sign in again',
},
{
statusCode: 403,
body: { code: 'FORBIDDEN' },
expected: 'You do not have access',
},
{
statusCode: 500,
body: { code: 'SERVER_ERROR' },
expected: 'Something went wrong',
},
]
for (const errorCase of cases) {
it(`handles ${errorCase.statusCode}`, () => {
cy.intercept('GET', '/api/reports/monthly', errorCase).as('getReport')
cy.visit('/reports/monthly')
cy.wait('@getReport')
.its('response.statusCode')
.should('eq', errorCase.statusCode)
cy.contains('[role=alert]', errorCase.expected).should('be.visible')
})
}
These are separate Mocha tests, so one failure does not stop later cases from being reported. Keep case data static at spec-definition time. Do not enqueue Cypress commands while dynamically generating test definitions.
Choose statuses from the product contract. A 422 body with field-level validation deserves its own detailed assertions, while 500 and 503 may intentionally share a generic view. For service-level negative testing beyond a stubbed browser, use API error handling and negative testing patterns.
7. Network Failure and Retry Button Example
An HTTP 500 response is not the same as a broken connection. Use forceNetworkError to destroy the browser connection and assert the transport-specific recovery UI.
it('retries after a transport failure', () => {
cy.intercept('GET', '/api/notifications', {
statusCode: 200,
body: { items: [{ id: 'n-1', text: 'Build completed' }] },
}).as('notificationsRecovered')
cy.intercept(
{ method: 'GET', url: '/api/notifications', times: 1 },
{ forceNetworkError: true },
).as('notificationsFailed')
cy.visit('/notifications')
cy.wait('@notificationsFailed').should('have.property', 'error')
cy.contains('button', 'Try again').should('be.visible').click()
cy.wait('@notificationsRecovered')
.its('response.statusCode')
.should('eq', 200)
cy.contains('Build completed').should('be.visible')
})
The fallback is registered first because normal intercept routes are evaluated in reverse definition order. The one-time failure is registered later, matches once, and then expires. This expresses the sequence without a mutable counter.
If the application retries automatically, wait for both aliases and assert the eventual state. Do not add a fixed wait that assumes the backoff duration unless time is itself the behavior being tested. Where possible, configure short retry timing in the application test environment.
8. Loading Skeleton with Delay
A controlled response delay makes a short-lived loading state observable. Assert the state before and after the aliased request completes.
it('shows a skeleton while recommendations load', () => {
cy.intercept('GET', '/api/recommendations', {
statusCode: 200,
delay: 600,
body: {
items: [
{ id: 'r-1', title: 'Add API contract checks' },
{ id: 'r-2', title: 'Split long-running specs' },
],
},
}).as('getRecommendations')
cy.visit('/dashboard')
cy.get('[data-cy=recommendation-skeleton]').should('be.visible')
cy.wait('@getRecommendations')
cy.get('[data-cy=recommendation-skeleton]').should('not.exist')
cy.get('[data-cy=recommendation-card]').should('have.length', 2)
})
The delay is illustrative and local to the response. It is not a performance threshold. Keep it only as large as needed to observe the initial state reliably.
For large streamed assets, throttleKbps can model a slower transfer. For most JSON state tests, delay is simpler and more predictable. If the product intentionally delays the skeleton to avoid flicker, assert that timer separately with cy.clock() and cy.tick() rather than increasing network latency until the assertion happens to work.
9. Pagination with Independent Aliases
Pagination tests should prove both the request cursor and the data merge or replacement behavior. Match each known cursor explicitly.
beforeEach(() => {
cy.intercept({
method: 'GET',
pathname: '/api/activity',
query: { limit: '2' },
}, {
body: {
items: [
{ id: 'e-1', text: 'Project created' },
{ id: 'e-2', text: 'Member invited' },
],
nextCursor: 'cursor-2',
},
}).as('getFirstActivityPage')
cy.intercept({
method: 'GET',
pathname: '/api/activity',
query: { limit: '2', cursor: 'cursor-2' },
}, {
body: {
items: [{ id: 'e-3', text: 'Test run passed' }],
nextCursor: null,
},
}).as('getSecondActivityPage')
})
it('appends the next activity page', () => {
cy.visit('/activity')
cy.wait('@getFirstActivityPage')
cy.get('[data-cy=activity-row]').should('have.length', 2)
cy.contains('button', 'Load more').click()
cy.wait('@getSecondActivityPage')
cy.get('[data-cy=activity-row]').should('have.length', 3)
cy.contains('Test run passed').should('be.visible')
cy.contains('button', 'Load more').should('not.exist')
})
The second route is narrower because it includes the cursor. Confirm the first matcher does not accidentally match the second request if the query subset is sufficient in your Cypress version and route definitions. An explicit initial matcher can include the absence semantics provided by the application URL, or a dynamic handler can branch on req.query.cursor.
10. GraphQL Query and Error Examples
Assign aliases based on operationName so one /graphql transport route can represent several business operations.
beforeEach(() => {
cy.intercept('POST', '/graphql', (req) => {
const operation = req.body.operationName
if (operation === 'WorkspaceMembers') {
req.alias = 'gqlWorkspaceMembers'
req.reply({
body: {
data: {
workspace: {
members: [
{ id: 'u-1', name: 'Morgan', role: 'owner' },
{ id: 'u-2', name: 'Riley', role: 'member' },
],
},
},
},
})
}
})
})
it('renders workspace members', () => {
cy.visit('/workspace/members')
cy.wait('@gqlWorkspaceMembers').then(({ request, response }) => {
expect(request.body.variables.workspaceId).to.eq('workspace-1')
expect(response?.body.errors).to.be.undefined
})
cy.get('[data-cy=member-row]').should('have.length', 2)
})
For an application-level error, return a GraphQL envelope:
req.reply({
statusCode: 200,
body: {
data: { workspace: null },
errors: [{ message: 'Workspace not found', path: ['workspace'] }],
},
})
That differs from a transport error such as HTTP 503. Test the branch the GraphQL client actually implements. Persisted queries may expose a stable hash instead of query text, so inspect the real request and match its supported identifier.
11. File Download Headers Example
Downloads often depend on status, content type, and content-disposition. Stub the browser request and assert that the application handles the response contract. Whether a browser saves a file can require separate download verification.
it('requests a CSV export', () => {
cy.intercept('GET', '/api/reports/export.csv', {
statusCode: 200,
headers: {
'content-type': 'text/csv; charset=utf-8',
'content-disposition': 'attachment; filename="quality-report.csv"',
},
body: 'name,status\nCheckout,passed\nSearch,failed\n',
}).as('exportReport')
cy.visit('/reports')
cy.contains('button', 'Export CSV').click()
cy.wait('@exportReport').then(({ response }) => {
expect(response?.headers['content-type']).to.contain('text/csv')
expect(response?.body).to.contain('Checkout,passed')
})
})
This pattern works when the application fetches the resource and creates a download. If clicking a normal navigation link bypasses the application request flow or changes the page, assert the link contract or use a suitable server and download test. Network stubbing should follow the actual implementation.
Binary bodies can use an ArrayBuffer or a fixture with an appropriate encoding. Avoid converting arbitrary binary data through text because that can produce an unrealistic response.
12. Real Response Inspection Without Replacing It
Sometimes the scenario needs a real integration but still benefits from alias-based synchronization and a focused response assertion. Use req.continue() when you need the response callback.
it('loads the real health summary', () => {
cy.intercept('GET', '/api/health/summary', (req) => {
req.continue((res) => {
expect(res.statusCode).to.eq(200)
expect(res.body).to.have.keys('services', 'updatedAt')
expect(res.body.services).to.be.an('array')
})
}).as('getHealthSummary')
cy.visit('/health')
cy.wait('@getHealthSummary')
cy.get('[data-cy=service-status]').should('have.length.greaterThan', 0)
})
The request reaches the upstream server. The test is now environment-dependent, so assertions should focus on stable contract properties instead of fixed shared data. Do not modify the response unless the test is explicitly about that transformation.
For a simple spy, omit the route handler entirely and assert the response yielded by cy.wait(). Reserve req.continue() for response-phase logic. Only one matching handler should call it for a request.
13. Production-Ready Cypress Network Stubbing Example Helper
A small helper can standardize the response envelope while leaving the scenario visible. Return the Cypress chain so command ordering and aliases remain correct.
type Page<T> = {
items: T[]
nextCursor: string | null
}
type Project = {
id: string
name: string
status: 'active' | 'archived'
}
function stubProjects(
response: Page<Project>,
alias = 'getProjects',
) {
return cy.intercept('GET', '/api/projects', {
statusCode: 200,
headers: { 'content-type': 'application/json' },
body: response,
}).as(alias)
}
it('shows archived projects', () => {
stubProjects({
items: [{ id: 'p-9', name: 'Legacy checkout', status: 'archived' }],
nextCursor: null,
})
cy.visit('/projects')
cy.wait('@getProjects')
cy.contains('Legacy checkout').should('be.visible')
cy.contains('Archived').should('be.visible')
})
Do not put cy.visit() or broad assertions in the stub helper. Callers should control when the request fires and what user behavior matters. The helper standardizes transport defaults, not the entire scenario.
If different tests require different query matchers, expose a typed options object or create domain-specific helpers. Avoid a universal mockApi(method, url, body, status) wrapper that removes domain meaning and gives every test unrestricted low-level control.
The conceptual guide how to use Cypress network stubbing explains how to choose between these recipes.
14. Verify Each Example Against Its Intended Boundary
Before committing a stub, run the application once against the real test service and inspect the request method, URL, headers, and body. Capture the response shape without copying sensitive records. Confirm status codes, empty-body behavior, and errors against the owned API documentation.
Review the finished test with a short checklist:
- The intercept exists before the trigger.
- The matcher cannot catch an unrelated call.
- The response represents the documented contract.
- The alias name describes the operation.
- The test asserts request details only where the UI owns them.
- The test proves the visible state after the response.
- Separate coverage checks the real service behavior.
Run the spec in both interactive and headless modes. A hidden dependency on service data can appear only in CI if an unstubbed secondary request still affects the view. Inspect the Routes panel and network log to find every call the scenario depends on.
Do not stub analytics, fonts, and unrelated traffic merely to make the test hermetic. Control dependencies that can change the result, and let harmless browser traffic remain outside the scenario. Too many global intercepts make it difficult to understand which response produced a failure.
Interview Questions and Answers
Q: Why must an intercept be registered before cy.visit()?
The page may send its request during initialization. A route created afterward cannot observe a request that has already happened. I register it first, then trigger the application behavior.
Q: How do you create sequential responses?
For fail-once behavior, I define a fallback route first and a later route with times: 1. For longer sequences, I use explicit state scoped to each test and reset it in beforeEach. I avoid randomness.
Q: Why assert the UI after cy.wait()?
The alias proves the exchange matched, but it does not prove the application rendered or handled it correctly. A browser test should assert the user-visible result as well as any relevant request contract.
Q: How do you stub GraphQL safely?
I inspect operationName or the actual persisted query identifier, set a request-specific alias, and reply only for that operation. I distinguish a GraphQL errors envelope from a transport failure.
Q: Do TypeScript generics validate a response?
No. They provide compile-time assistance inside the test but do not validate JSON at runtime. I validate fixtures and service responses against the owned schema elsewhere.
Q: When is a fixture better than inline data?
A fixture is better for a large, stable payload reused across scenarios. Inline data is clearer for small scenario-specific responses. Typed builders work well when many tests vary a few fields.
Q: What does forceNetworkError test?
It destroys the request connection and exercises transport failure behavior. It is distinct from an HTTP response such as 500, which has a status and response body.
Common Mistakes
- Defining the intercept after page initialization or the triggering action.
- Reusing the alias
apifor several unrelated operations. - Matching only a path when GET and POST share that path.
- Returning a success status or body the real API never uses.
- Loading fixtures from inside the network callback.
- Using one mutable counter across tests and leaking response sequence state.
- Forgetting that normal intercept overrides use reverse registration order.
- Treating GraphQL HTTP 200 as proof that the operation succeeded.
- Asserting only stub internals and never the user's view.
- Assuming compile-time types validate JSON fixtures at runtime.
Conclusion
Choose the Cypress network stubbing example closest to the behavior you need, then replace the routes and payloads with your actual contract. Keep registration ahead of the trigger, use a precise matcher, and assert the resulting UI rather than stopping at the intercepted response.
Begin with the minimal static response, add dynamic behavior only when the request demands it, and preserve real integration coverage. For related test setup patterns, see Cypress cy.request API examples.
Interview Questions and Answers
Show the complete flow of a network stub.
I register a precise cy.intercept with a contract-accurate response and alias before the trigger. I perform the user action, wait on the alias, inspect relevant request or response fields, and assert the rendered outcome.
How would you model a retry that succeeds?
I register the success fallback, then a one-time failure route with times: 1 so it matches first. After triggering the request, I wait for the failure, exercise or observe retry, wait for success, and assert recovery.
What makes a good RouteMatcher?
It is narrow enough to match only the intended business call but not coupled to volatile data. I usually specify method and pathname, then add query or stable headers when they drive the scenario.
How do you choose between inline bodies, fixtures, and builders?
I use inline bodies for short single-scenario data, fixtures for large reusable documents, and typed builders when tests vary a few fields. In every case the payload stays synthetic and aligned with the service contract.
How do you prove a GraphQL stub matched the right operation?
I inspect operationName or the persisted query identifier and assign req.alias only for the target operation. The test waits on that per-request alias and asserts variables plus the GraphQL response envelope.
Can a stubbed browser test validate the backend?
No. It validates frontend behavior for the supplied response. Backend behavior needs direct API or contract tests and selected non-stubbed browser journeys.
How do you test a loading state without a fixed wait?
I add a short deterministic delay to the stub, assert the loading indicator immediately, wait on the route alias, then assert that the indicator disappears and content renders. The delay exposes state and is not a performance measurement.
What is the risk of a universal API mock helper?
It can hide business intent, permit unrealistic responses, and make route ownership unclear. I prefer small domain-specific helpers that preserve the alias, trigger, and visible assertions in the spec.
Frequently Asked Questions
What is the simplest Cypress network stubbing example?
Call cy.intercept with the method, URL, and a StaticResponse, then alias it before visiting the page. Wait for the alias and assert the visible result.
How do I return different responses from the same Cypress route?
Use a route handler and call req.reply based on stable request data. For a one-time response, use a RouteMatcher with times set to one and define a fallback route.
How do I stub a Cypress response from a fixture?
Pass a StaticResponse with the fixture property, such as { fixture: 'folder/file.json' }. If you need to modify it, load the fixture first, then register the intercept with the changed body.
Can cy.intercept assert a POST body?
Yes. Alias the POST intercept and inspect request.body from the object yielded by cy.wait. Assert fields owned by the UI, then verify the resulting user-visible state.
How do I stub only the first Cypress request?
Add times: 1 to a RouteMatcher. Register a fallback route first and the one-time override later because normal routes are evaluated in reverse definition order.
How do I mock a GraphQL error in Cypress?
Intercept the GraphQL operation and return HTTP 200 with a body containing the appropriate errors array and data shape. Use a transport status such as 503 only when testing a transport failure.
Why is my stubbed response not used?
The request may have happened before route registration, failed to match method or URL, been served from cache, or matched another intercept. Inspect the actual request and route order before widening the matcher.