Test data has to live somewhere. Inline arrays clutter your specs; per-test database seeding is slow; hardcoding values across files turns refactors into grep-and-replace marathons. Fixtures — static files in cypress/fixtures/ — are Cypress's answer. This lesson is about getting fixtures organised, typed, and wired to both cy.intercept and cy.request so test data has one source of truth across your suite.
What fixtures are
A fixture is a file in cypress/fixtures/. Cypress reads it from disk and yields the parsed contents through cy.fixture("filename"). Supported formats out of the box:
- JSON — by far the most common; auto-parsed.
- Plain text and CSV — yielded as a string, you parse it.
- Images, PDFs, binaries — yielded as a base64 string or buffer; useful for
selectFileuploads.
Fixtures are checked into git, version-controlled with your tests, and live next to the specs that use them. They're the right home for deterministic, public, reusable test data — and the wrong home for secrets (which belong in env vars) or anything that has to be unique per run (timestamps, UUIDs).
Loading a fixture in a test
The basic shape:
cy.fixture("user.json").then((user) => {
cy.get("[data-testid='email']").type(user.email);
cy.get("[data-testid='password']").type(user.password);
cy.get("[data-testid='submit']").click();
});cy.fixture("user.json") reads cypress/fixtures/user.json and yields the parsed JSON. The .then callback receives the parsed value. The same call also works without the .json extension — cy.fixture("user") is equivalent for JSON files.
cy.fixture runs once per test invocation and caches the result for the rest of the spec; reading the same fixture twice is free.
The most common pattern — fixture as stub
Fixtures and cy.intercept are designed to work together. Pass { fixture: "name.json" } and the file becomes the stubbed response body:
cy.intercept("GET", "/api/products", { fixture: "products.json" })
.as("getProducts");
cy.visit("/products");
cy.wait("@getProducts");
cy.get("[data-testid='product-card']").should("have.length", 6);products.json lives in cypress/fixtures/; Cypress reads, parses, and ships it as the response body. The product list test now has stable data on every run, no inline mock objects in the spec, and the JSON is browsable in any editor.
This is the canonical use of fixtures. If you find yourself typing a 30-line array literal into a spec for stubbing, move it to a fixture.
Organising fixtures by feature
A flat fixtures folder breaks down past about twenty files. Group by feature instead:
cypress/fixtures/
├── users/
│ ├── admin.json
│ ├── standard-user.json
│ └── invalid-user.json
├── products/
│ ├── full-list.json
│ ├── empty.json
│ └── single.json
└── api-responses/
├── login-success.json
├── login-error.json
└── server-error.json
Reference them with the path:
cy.intercept("GET", "/api/products", { fixture: "products/full-list.json" });
cy.fixture("users/admin.json").then((admin) => { ... });Group by feature, not by HTTP method or by status code alone. The folder structure should mirror the parts of your app that change together.
Typing fixtures with TypeScript
Fixtures yield unknown by default — the compiler has no way to know what's in a JSON file at type-check time. Annotate the value yourself:
interface UserFixture {
name: string;
email: string;
password: string;
role: "admin" | "standard" | "tester";
}
cy.fixture<UserFixture>("users/admin.json").then((user) => {
// user is fully typed — autocomplete on email, password, role
expect(user.role).to.equal("admin");
cy.get("[data-testid='email']").type(user.email);
});cy.fixture<T> accepts a generic that types the resolved value. Pair it with a shared interface (in cypress/support/types.ts is a common spot) so every test that loads the fixture sees the same shape.
If the fixture is ever out of sync with the type — a missing field, a renamed key — TypeScript flags it the moment a test reads it. This is the single most valuable habit for keeping a fixture-heavy suite honest as the API evolves. Chapter 9 covers the types-folder pattern in depth.
Aliases for fixtures used in multiple tests
Loading a user fixture in five tests with five cy.fixture(...) calls is repetitive. Alias it once in beforeEach:
beforeEach(() => {
cy.fixture<UserFixture>("users/admin.json").as("admin");
});
it("logs in as admin", function () {
cy.get("[data-testid='email']").type(this.admin.email);
cy.get("[data-testid='password']").type(this.admin.password);
cy.get("[data-testid='submit']").click();
});Two requirements that catch most beginners:
- Use a
function()callback, not an arrow function.this.adminonly resolves correctly when Mocha'sthiscontext is preserved — arrow functions inheritthisfrom the surrounding scope and break it. - The alias name (
"admin") becomesthis.admininside the test. Standard Mocha context-sharing.
If the function() vs () => rule trips you up, an alternative is to assign the fixture to a typed variable inside the .then:
let admin: UserFixture;
beforeEach(() => {
cy.fixture<UserFixture>("users/admin.json").then((u) => {
admin = u;
});
});
it("logs in as admin", () => {
cy.get("[data-testid='email']").type(admin.email);
});Same behaviour, no function() requirement. Pick whichever your team finds clearer.
Dynamic data on top of a fixture
Fixtures are static. Some tests need a unique value per run — an email that hasn't been registered before, an order ID that doesn't collide. Load the fixture, then spread the unique fields on top:
cy.fixture<UserFixture>("users/standard-user.json").then((base) => {
const unique: UserFixture = {
...base,
email: `test-${Date.now()}@example.com`,
};
cy.request("POST", "/api/users", unique).then((response) => {
expect(response.status).to.equal(201);
});
});This is the data-factory pattern in miniature — fixture as the template, code as the variation. Chapter 9 covers the full factory pattern with @faker-js/faker and shared TypeScript types.
Fixtures and cy.request for setup
Stubbing isn't the only place fixtures earn their keep. Use them as request bodies for backend setup:
beforeEach(() => {
cy.fixture<UserFixture>("users/admin.json").then((admin) => {
cy.request("POST", "/api/test/users", admin);
});
});Now every test in the suite starts with the admin user freshly created in the database — the spec stays clean and the seed data has one source of truth in users/admin.json.
Don't put secrets in fixtures
cypress/fixtures/ is checked into git. Anything in it is public to anyone who can clone the repo. Keep these out of fixtures:
- Production passwords or API keys
- Personally identifiable data (real names, real emails) for compliance reasons
- Tokens, session secrets, anything that would let someone log in elsewhere
Use Cypress env vars (Cypress.env("PROD_API_KEY"), sourced from cypress.env.json or CYPRESS_PROD_API_KEY shell var) for credentials. cypress.env.json should be in .gitignore.
Fixture flow at a glance
⚠️ Common mistakes
- Using arrow functions with fixture aliases.
beforeEach(() => cy.fixture("user").as("user"))thenit("...", () => this.user.email)—this.userisundefinedbecause arrow functions don't bindthis. Use thefunction()form for the test body, or assign the fixture to a typed variable in the.thencallback. - Hardcoding fixture values that change frequently in
cypress/fixtures/. A fixture that names "the price of product 42 in November" is a fixture that stops being true in December. Either generate the dynamic part in code (timestamps, dates, unique emails) or keep the fixture stable and assert on patterns rather than literals. - Putting real-environment credentials into a JSON fixture. Fixtures are checked into git. Treat them as public data. Use
cypress.env.json(gitignored) or shell env vars for secrets — and document the convention in your project README so new engineers don't accidentally commit a key.
🎯 Practice task
Wire up a fully fixture-driven test suite. 25-30 minutes.
- In
cypress/fixtures/, create a feature-grouped tree:users/admin.json,users/standard.json,products/full.json,products/empty.json, andapi-responses/login-error.json. - Define the matching TypeScript interfaces in
cypress/support/types.ts:UserFixture,Product,LoginErrorResponse. - Create
cypress/e2e/fixtures.cy.tswith three tests:- Login from fixture —
cy.fixture<UserFixture>("users/admin.json").then((admin) => ...), type admin's email/password, click submit, assert the URL changes. - Stubbed product list —
cy.intercept("GET", "/api/products", { fixture: "products/full.json" }), visit/products, assert the rendered count matches the fixture length. - Stubbed empty state — same intercept but with
{ fixture: "products/empty.json" }, assert the empty state component renders.
- Login from fixture —
- Alias drill — refactor the first test to load the admin fixture in
beforeEachwith.as("admin"), and usefunction() { this.admin.email }insideit. Confirm the test still passes. Then refactor it to the typed-variable approach (let admin: UserFixture; beforeEach(... admin = u)) and confirm both forms work. - Dynamic-data drill — load
users/standard.json, spread{ ...base, email: \test-$1782220040803@example.com` }, send the result viacy.request("POST", "/api/users", uniqueUser)`. The fixture stays the template; uniqueness is generated at runtime. - Stretch: add a fixture for a 500 server error (
api-responses/server-error.json), wire it to a fourth test that stubs the products endpoint with{ statusCode: 500, fixture: "api-responses/server-error.json" }, and assert the UI's error banner appears. This closes the loop between the four states from lesson 2 and the fixture organisation in this lesson.
That ends chapter 4 — the network half of Cypress. You can now spy on requests, stub responses, sequence multi-call flows, fire your own backend calls, and feed everything from fixture files. The next chapter goes meta: custom commands, page objects, app actions, and the framework patterns that turn a spec folder into a test framework.