cy.intercept lets a Cypress test catch any HTTP request the app makes, then either watch it or answer it with data you control. Combined with fixtures and a few mock data factories built on Faker, it gives you front-end tests that don't depend on a shared backend, don't break when someone edits the staging database, and can reach error and edge cases on demand.
This guide builds that setup step by step: the core cy.intercept patterns, a small interceptApi helper, fixtures, factories with @faker-js/faker, intercepting requests with query strings, waiting on aliases, a role-based visitAs login helper, and asserting on form data and file uploads. It assumes you know the basics from the Cypress E2E testing guide.
cy.intercept basics
cy.intercept takes a route matcher and, optionally, a response. Without a response it only spies on the request:
// Spy: let the request through, but give it an alias to wait on
cy.intercept('GET', '/api/words').as('getWords');
// Stub with a static body
cy.intercept('GET', '/api/words', { body: { words: [] } });
// Stub with a status code, headers and a delay
cy.intercept('GET', '/api/words', {
statusCode: 503,
body: { message: 'Service unavailable' },
delay: 500,
});
// Stub with a fixture file from cypress/fixtures
cy.intercept('GET', '/api/words', { fixture: 'words.json' });
// Decide the response from the request
cy.intercept('POST', '/api/words', (req) => {
req.reply({ statusCode: 201, body: { id: 99, ...req.body } });
});Three rules prevent most confusion:
- Register intercepts before the request fires. Put them before
cy.visit, or before the click that triggers the call. - The most recently defined matching intercept wins, so a
beforeEachdefault can be overridden inside a single test. - Intercepts are reset between tests, so there's no cleanup to do.
A typed interceptApi helper
Most stubs in a project follow the same shape: a method, an API path, a JSON body and a status code. A custom command removes the repetition and keeps the API base URL in one place:
// cypress/support/commands.ts
Cypress.Commands.add(
'interceptApi',
(method: string, path: string, body: unknown, statusCode = 200) => {
return cy.intercept(
{ method, url: `${Cypress.env('apiUrl')}${path}` },
{ statusCode, body },
);
},
);
declare global {
namespace Cypress {
interface Chainable {
interceptApi(
method: string,
path: string,
body: unknown,
statusCode?: number,
): Chainable<null>;
}
}
}
export {};Set apiUrl in cypress.config.ts under env, or with a CYPRESS_apiUrl environment variable in CI. Tests then read like a description of the scenario:
cy.interceptApi('GET', '/words', { words: [] }).as('getWords');
cy.interceptApi('POST', '/login', { message: 'Invalid credentials' }, 401);Fixtures: fixed data from files
Fixtures are JSON files in cypress/fixtures. They suit data that should look the same in every run, such as a known list you assert against item by item:
{
"words": [
{ "id": 1, "word": "Ipsum1" },
{ "id": 2, "word": "Ipsum2" },
{ "id": 3, "word": "Lorem1" },
{ "id": 4, "word": "Lorem2" }
]
}describe('Word list', () => {
beforeEach(() => {
cy.intercept('GET', '/api/words', { fixture: 'words.json' }).as('getWords');
cy.visit('/words');
cy.wait('@getWords');
});
it('renders every word in order', () => {
cy.fixture('words.json').then(({ words }) => {
cy.findAllByRole('listitem').should('have.length', words.length);
words.forEach(({ word }: { word: string }, index: number) => {
cy.findAllByRole('listitem').eq(index).should('have.text', word);
});
});
});
});Loading the same fixture with cy.fixture for the assertions means the test and the stub can't drift apart. The findAllByRole query comes from Cypress Testing Library.
Mock data factories with Faker
Fixtures get awkward when you need 25 users for a pagination test, or one user with a specific role. Factories solve that: functions that build a valid object with realistic random values, and let each test override only the fields it cares about.
Install the maintained Faker package. The old faker package on npm is abandoned; @faker-js/faker is the community fork that replaced it, and its API changed along the way.
npm install --save-dev @faker-js/faker// cypress/support/factories.ts
import { faker } from '@faker-js/faker';
export type Role = 'admin' | 'editor' | 'viewer';
export interface User {
id: string;
name: string;
email: string;
role: Role;
createdAt: string;
}
export const createUser = (overrides: Partial<User> = {}): User => ({
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email().toLowerCase(),
role: faker.helpers.arrayElement<Role>(['admin', 'editor', 'viewer']),
createdAt: faker.date.past().toISOString(),
...overrides,
});
export interface Article {
id: number;
title: string;
body: string;
categoryId: number;
}
export const createArticle = (overrides: Partial<Article> = {}): Article => ({
id: faker.number.int({ min: 1, max: 100_000 }),
title: faker.lorem.sentence({ min: 3, max: 8 }),
body: faker.lorem.paragraphs(3),
categoryId: faker.number.int({ min: 1, max: 5 }),
...overrides,
});To build a list, use faker.helpers.multiple or lodash's times, which Cypress bundles as Cypress._:
const users = faker.helpers.multiple(() => createUser(), { count: 25 });
const admins = Cypress._.times(3, () => createUser({ role: 'admin' }));If you learned Faker from older tutorials, many method names have moved:
Old faker (deprecated) | @faker-js/faker today |
|---|---|
faker.datatype.number({ min: 1, max: 5 }) | faker.number.int({ min: 1, max: 5 }) |
faker.datatype.float() | faker.number.float({ fractionDigits: 2 }) |
faker.datatype.uuid() | faker.string.uuid() |
faker.datatype.datetime() | faker.date.anytime() |
faker.name.findName() | faker.person.fullName() |
faker.random.arrayElement([...]) | faker.helpers.arrayElement([...]) |
Keep random data reproducible
Random data can find bugs that fixed data hides, but a failure you can't reproduce is frustrating. Seed Faker so each run is repeatable:
// cypress/support/e2e.ts
import { faker } from '@faker-js/faker';
beforeEach(() => {
faker.seed(12345);
});Also avoid asserting on hard-coded text when the data is generated. Keep a reference to the generated object and assert against its fields.
Endpoint helpers and query strings
Put the intercept for each endpoint in one helper that returns both the data and the route. List endpoints usually take filters and pagination through the query string:
// cypress/support/api/articles.ts
import { faker } from '@faker-js/faker';
import { createArticle, type Article } from '../factories';
export type ArticleFilter = Partial<{ term: string; categoryId: number; page: number }>;
export const mockGetArticles = (
filter: ArticleFilter = {},
articles: Article[] = faker.helpers.multiple(() => createArticle(), { count: 10 }),
total = articles.length,
) => {
const query = Cypress._.mapValues(
Cypress._.omitBy(filter, Cypress._.isNil),
String,
);
const response = { items: articles, total, page: filter.page ?? 1 };
cy.intercept({ method: 'GET', pathname: '/api/articles', query }, { body: response })
.as('getArticles');
return response;
};Matching with pathname and query is sturdier than building the full URL string yourself: parameter order doesn't matter, and you don't need to URL-encode search terms. Query values are compared as strings, which is why the helper converts them with String.
A search-and-paging test then only states what changes:
it('searches and pages through articles', () => {
mockGetArticles();
cy.visit('/articles');
cy.wait('@getArticles');
const results = mockGetArticles({ term: 'cypress' });
cy.findByRole('searchbox', { name: 'Search' }).type('cypress{enter}');
cy.wait('@getArticles');
cy.findAllByRole('article').should('have.length', results.items.length);
mockGetArticles({ term: 'cypress', page: 2 });
cy.findByRole('button', { name: 'Next page' }).click();
cy.wait('@getArticles').its('request.query.page').should('eq', '2');
});Each call re-registers the @getArticles alias with a more specific matcher, and because the newest intercept wins, each cy.wait receives the request from the step it follows.
Waiting on aliases with cy.wait
cy.wait('@alias') pauses until a matching request completes, then yields the interception: the request, the response and timing. Use it for two things.
First, synchronization. Don't use cy.wait(2000). Waiting on the actual request is faster and doesn't break on a slow CI machine.
Second, assertions on what the app sent:
cy.wait('@createWord').then(({ request, response }) => {
expect(request.body).to.deep.equal({ word: 'Dolor' });
expect(request.headers).to.have.property('authorization');
expect(response?.statusCode).to.eq(201);
});If the same request fires several times, call cy.wait('@alias') once per request, or cy.wait(['@getUser', '@getSettings']) to wait for several routes.
Role-based login with a visitAs helper
For role-based access control, most UI tests don't need to go through the sign-in form. They need the app to believe a user with a given role is signed in. Stub the profile endpoint and put a token where the app looks for it:
// cypress/support/auth.ts
import { faker } from '@faker-js/faker';
import { createUser, type Role, type User } from './factories';
export const visitAs = (role: Role, path: string): User => {
const user = createUser({ role });
cy.intercept('GET', '/api/me', { body: user }).as('getProfile');
cy.visit(path, {
onBeforeLoad(win) {
win.localStorage.setItem('accessToken', faker.string.uuid());
},
});
cy.wait('@getProfile');
return user;
};onBeforeLoad writes the token into the app's own window before any app code runs, so there's no race with the app's startup. Permission tests become short and table-like:
it('shows user management to admins', () => {
visitAs('admin', '/settings');
cy.findByRole('link', { name: 'Manage users' }).should('be.visible');
});
it('hides user management from viewers', () => {
visitAs('viewer', '/settings');
cy.findByRole('link', { name: 'Manage users' }).should('not.exist');
});
it('redirects to sign-in when the token is rejected', () => {
cy.intercept('GET', '/api/me', { statusCode: 401 });
cy.visit('/settings');
cy.location('pathname').should('eq', '/login');
});Keep a few real sign-in tests against a real backend for the authentication flow itself. These stubs test the UI's behavior per role, not the security of the API. Access rules must still be enforced on the server.
Intercepting form data and file uploads
How you assert on a request body depends on its content type.
JSON is parsed for you, so request.body is an object, as in the cy.wait example above.
URL-encoded forms (application/x-www-form-urlencoded) arrive as a string. Parse it with URLSearchParams:
cy.intercept('POST', '/api/contact').as('contact');
// ...fill in and submit the form...
cy.wait('@contact').then(({ request }) => {
const form = new URLSearchParams(request.body);
expect(form.get('email')).to.eq('[email protected]');
expect(form.get('message')).to.contain('Hello');
});Multipart uploads (multipart/form-data) contain file bytes, so the body may be a string or a binary buffer. Decode it and check the parts you care about:
cy.intercept('POST', '/api/avatar', { statusCode: 201, body: { url: '/avatars/1.png' } })
.as('uploadAvatar');
cy.findByLabelText('Profile photo').selectFile('cypress/fixtures/avatar.png');
cy.findByRole('button', { name: 'Upload' }).click();
cy.wait('@uploadAvatar').then(({ request }) => {
expect(request.headers['content-type']).to.include('multipart/form-data');
const body =
typeof request.body === 'string'
? request.body
: new TextDecoder().decode(request.body);
expect(body).to.include('name="avatar"; filename="avatar.png"');
});
cy.findByRole('img', { name: 'Profile photo' })
.should('have.attr', 'src', '/avatars/1.png');This confirms that the right field name and file name were sent, which catches the common bug where the front end and the API disagree about the form field.
FAQ
What is the difference between cy.intercept and cy.request?
cy.intercept catches requests that the app makes from the browser and can stub or inspect them. cy.request sends a request directly from the test, for example to seed data or log in through an API. They solve different problems and often appear in the same suite.
Should I use fixtures or Faker?
Use fixtures when the test asserts specific values that should never change. Use factories with Faker when you need many records, variations per test, or realistic data shapes. Many suites combine them: a factory for most data, a fixture for a few golden responses.
How do I intercept a request with query parameters in Cypress?
Pass a route matcher object with pathname and query, for example { pathname: '/api/articles', query: { page: '2' } }. A plain string such as '/api/articles' is looser than it looks and can also catch /api/articles/5 or requests with any query string. Query values are compared as strings.
Is the faker npm package still maintained?
No. Use @faker-js/faker. Methods moved into modules such as faker.number, faker.string and faker.person, so older examples need updating.
Checklist
- Register every
cy.interceptbefore the request fires, and give it an alias. - Wrap common stubs in an
interceptApicommand and per-endpoint helpers. - Use fixtures for golden data and
@faker-js/fakerfactories with overrides for everything else. Seed Faker. - Match list endpoints with
pathnameandquery. - Replace
cy.wait(ms)withcy.wait('@alias'), and assert on the request it yields. - Use a
visitAs(role, path)helper for permission tests, and keep a few real sign-in tests. - Decode form and multipart bodies before asserting on them.
The same intercepts work in Cypress component testing, so a factory you write once serves both suites. If you want a second pair of hands on a front-end test strategy, Vectorkub builds and maintains production web applications.
