Free Ebook cover Cypress End-to-End Testing Basics: Stable UI Tests for Web Apps

Cypress End-to-End Testing Basics: Stable UI Tests for Web Apps

New course

10 pages

Fixtures and Test Data in Cypress: Repeatable Inputs Without Flakiness

Capítulo 5

Estimated reading time: 10 minutes

+ Exercise

When to Use Fixtures (and When Not To)

Fixtures are files (usually JSON) that store test data you want to reuse across runs. They help you feed the app consistent inputs so tests stay repeatable and don’t depend on whatever data happens to exist in a database or API at the moment.

Good uses for fixtures

  • Static API responses you want to control: product lists, feature flags, user profiles, empty states, error payloads.
  • Form inputs you want to keep consistent: valid/invalid registration data, address formats, edge cases.
  • UI rendering checks where the page should display exactly what the data contains (names, prices, badges).
  • Boundary scenarios: zero items, one item, many items, missing optional fields.

When fixtures are not the best tool

  • Highly dynamic data that must be generated per run (timestamps, random IDs). Prefer generating in the test or using factories.
  • End-to-end flows that must hit real services (e.g., verifying an integration). In that case, fixtures can still be used for seeding, but not for replacing the whole system under test.
  • Large datasets that make tests slow and hard to understand. Prefer small, scenario-focused fixtures.

A stable pattern is: use fixtures to control inputs and stub responses, then assert on the UI behavior that should result from those controlled inputs.

Loading Fixtures with cy.fixture() and Aliasing

Cypress loads fixtures from cypress/fixtures. The most common workflow is: load a fixture, alias it, then use it later in the test (or in intercept handlers).

Step-by-step: load and use a fixture

// cypress/e2e/profile.cy.js

describe('Profile page', () => {
  it('renders the user name from fixture data', () => {
    cy.fixture('users/jane.json').as('user');

    cy.get('@user').then((user) => {
      // Example: fill a form using fixture data
      cy.get('[data-cy=first-name]').clear().type(user.firstName);
      cy.get('[data-cy=last-name]').clear().type(user.lastName);
    });
  });
});

Aliasing with .as() makes the fixture available via cy.get('@alias'). This keeps your test readable and avoids reloading the same file multiple times.

Using fixtures with network stubbing

A very common use is to stub an API response with fixture data so the UI always receives the same payload.

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

// cypress/e2e/products.cy.js

describe('Products', () => {
  it('renders products from a stubbed API response', () => {
    cy.fixture('products/basic-list.json').as('products');

    cy.intercept('GET', '/api/products', (req) => {
      req.reply({
        statusCode: 200,
        body: Cypress._.cloneDeep(this.products) // not valid here; see note below
      });
    });
  });
});

The snippet above shows the intent, but note that this.products only works if you use function () {} (not arrow functions) and assign the alias to this via cy.fixture(...).then(data => { this.products = data }). A simpler and less error-prone approach is to reply directly with the fixture file:

it('renders products from a stubbed API response', () => {
  cy.intercept('GET', '/api/products', { fixture: 'products/basic-list.json' }).as('getProducts');

  cy.visit('/products');
  cy.wait('@getProducts');

  cy.get('[data-cy=product-card]').should('have.length', 3);
});

Using { fixture: '...' } avoids timing issues and keeps the intercept setup concise.

Parameterizing Tests with Fixture Variants

Fixture variants are multiple small fixture files representing different scenarios. Instead of one huge products.json, create files like empty.json, basic-list.json, with-sale-items.json, and server-error.json. Then parameterize tests to run the same assertions across variants.

Pattern: table-driven tests

// cypress/e2e/products-variants.cy.js

const scenarios = [
  {
    name: 'empty list',
    fixture: 'products/empty.json',
    expectedCount: 0,
    expectedEmptyState: true,
  },
  {
    name: 'basic list',
    fixture: 'products/basic-list.json',
    expectedCount: 3,
    expectedEmptyState: false,
  },
];

describe('Products page scenarios', () => {
  scenarios.forEach((s) => {
    it(`renders correctly for ${s.name}`, () => {
      cy.intercept('GET', '/api/products', { fixture: s.fixture }).as('getProducts');

      cy.visit('/products');
      cy.wait('@getProducts');

      cy.get('[data-cy=product-card]').should('have.length', s.expectedCount);

      if (s.expectedEmptyState) {
        cy.get('[data-cy=products-empty]').should('be.visible');
      } else {
        cy.get('[data-cy=products-empty]').should('not.exist');
      }
    });
  });
});

This approach keeps each test focused on behavior while letting you expand coverage by adding a new fixture file and a new scenario entry.

Variant design tips

  • Change one dimension at a time (e.g., empty vs non-empty, sale flag on/off).
  • Name variants by outcome (what the UI should do), not by internal implementation.
  • Prefer explicit edge cases: missing optional fields, long names, zero price, out-of-stock.

Avoiding Hidden Coupling Between Fixture Data and UI Assumptions

Fixtures can accidentally make tests brittle if the test relies on specific values that are not essential to the behavior being tested. This is “hidden coupling”: the test passes only because the fixture happens to match the UI’s current structure or copy.

Common coupling traps

  • Asserting exact full text that includes dynamic formatting (currency, locale, punctuation) when you only need to assert presence of key values.
  • Relying on array order when the UI sorts differently or the backend changes ordering.
  • Using fixture IDs as selectors (e.g., #product-123) instead of stable test attributes.
  • Assuming optional fields always exist because the fixture always includes them.

Make assertions match the behavior, not the fixture details

Example: if you want to verify that the UI renders product names and that clicking “Add to cart” updates the cart count, you don’t need to assert every field in the payload.

// Better: assert key behavior
cy.get('[data-cy=product-card]').first().within(() => {
  cy.get('[data-cy=product-name]').should('not.be.empty');
  cy.get('[data-cy=add-to-cart]').click();
});
cy.get('[data-cy=cart-count]').should('have.text', '1');

If you do need to assert a specific value, pull it from the fixture rather than hardcoding it in the test. That way, the test stays aligned with the scenario data.

cy.fixture('products/basic-list.json').then((products) => {
  cy.intercept('GET', '/api/products', { body: products }).as('getProducts');
  cy.visit('/products');
  cy.wait('@getProducts');

  cy.get('[data-cy=product-card]').eq(0).within(() => {
    cy.get('[data-cy=product-name]').should('have.text', products[0].name);
  });
});

Avoid order coupling

If the UI sorts products alphabetically, but your fixture is in a different order, asserting “first item equals X” will be flaky. Prefer assertions that search within the list.

// Avoid: depends on order
cy.get('[data-cy=product-card]').first().should('contain', 'Coffee');

// Prefer: order-independent
cy.get('[data-cy=product-list]').should('contain.text', 'Coffee');

Keeping Fixtures Small, Focused, and Named by Scenario

Small fixtures are easier to read, easier to update, and less likely to accidentally encode irrelevant details. A good fixture answers: “What is the minimal data needed to produce this UI scenario?”

Recommended folder structure

cypress/fixtures/
  users/
    jane.basic.json
    jane.missing-avatar.json
    admin.basic.json
  products/
    empty.json
    basic-list.json
    with-sale-items.json
    out-of-stock.json

Naming guidelines

  • Use scenario names: out-of-stock.json is clearer than products2.json.
  • Keep files short: prefer 1–10 items, not 500.
  • Include only fields the UI needs for the scenario (plus required API fields).
  • Document intent through structure: if a product is on sale, include a clear onSale flag and a salePrice.

Mini-Lab: Fixtures for User Profiles and Product Lists

In this mini-lab you will create two sets of fixtures and write tests that validate rendering and behavior using controlled data. The goal is repeatable UI tests that do not depend on live backend state.

Lab setup: create fixture files

Create these files under cypress/fixtures.

1) User profile fixtures

// cypress/fixtures/users/jane.basic.json
{
  "id": "u_1001",
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "jane.doe@example.test",
  "role": "user",
  "avatarUrl": "https://example.test/avatars/jane.png"
}
// cypress/fixtures/users/jane.missing-avatar.json
{
  "id": "u_1001",
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "jane.doe@example.test",
  "role": "user",
  "avatarUrl": null
}

2) Product list fixtures

// cypress/fixtures/products/basic-list.json
[
  { "id": "p_1", "name": "Coffee", "price": 12.5, "inStock": true, "onSale": false },
  { "id": "p_2", "name": "Tea", "price": 9.0, "inStock": true, "onSale": true, "salePrice": 7.5 },
  { "id": "p_3", "name": "Mug", "price": 6.0, "inStock": false, "onSale": false }
]
// cypress/fixtures/products/empty.json
[]

Lab tests: profile rendering from fixture data

This test stubs the profile endpoint so the UI always renders the same user details. Adjust the URL paths to match your app (the technique stays the same).

// cypress/e2e/profile-fixtures.cy.js

describe('Profile with fixtures', () => {
  it('renders name and email for a basic user profile', () => {
    cy.intercept('GET', '/api/me', { fixture: 'users/jane.basic.json' }).as('getMe');

    cy.visit('/profile');
    cy.wait('@getMe');

    cy.get('[data-cy=profile-name]').should('contain.text', 'Jane');
    cy.get('[data-cy=profile-name]').should('contain.text', 'Doe');
    cy.get('[data-cy=profile-email]').should('have.text', 'jane.doe@example.test');
  });

  it('shows a fallback avatar when avatarUrl is missing', () => {
    cy.intercept('GET', '/api/me', { fixture: 'users/jane.missing-avatar.json' }).as('getMe');

    cy.visit('/profile');
    cy.wait('@getMe');

    cy.get('[data-cy=profile-avatar-fallback]').should('be.visible');
  });
});

Lab tests: product list rendering and behavior

Now stub the products endpoint and validate both rendering (cards, labels) and behavior (e.g., add-to-cart disabled when out of stock).

// cypress/e2e/products-fixtures.cy.js

describe('Products with fixtures', () => {
  it('renders product cards and sale badge based on fixture data', () => {
    cy.intercept('GET', '/api/products', { fixture: 'products/basic-list.json' }).as('getProducts');

    cy.visit('/products');
    cy.wait('@getProducts');

    cy.get('[data-cy=product-card]').should('have.length', 3);

    // Sale item should show a sale badge (based on fixture onSale=true)
    cy.contains('[data-cy=product-card]', 'Tea').within(() => {
      cy.get('[data-cy=product-sale-badge]').should('be.visible');
      cy.get('[data-cy=product-price]').should('contain.text', '7.5');
    });
  });

  it('disables add-to-cart for out-of-stock items', () => {
    cy.intercept('GET', '/api/products', { fixture: 'products/basic-list.json' }).as('getProducts');

    cy.visit('/products');
    cy.wait('@getProducts');

    cy.contains('[data-cy=product-card]', 'Mug').within(() => {
      cy.get('[data-cy=stock-status]').should('contain.text', 'Out of stock');
      cy.get('[data-cy=add-to-cart]').should('be.disabled');
    });
  });

  it('shows an empty state when the product list is empty', () => {
    cy.intercept('GET', '/api/products', { fixture: 'products/empty.json' }).as('getProducts');

    cy.visit('/products');
    cy.wait('@getProducts');

    cy.get('[data-cy=product-card]').should('have.length', 0);
    cy.get('[data-cy=products-empty]').should('be.visible');
  });
});

Lab extension: parameterize the product scenarios

Once the basics work, refactor the product tests into a scenario table so adding a new fixture automatically adds coverage.

// cypress/e2e/products-scenarios.cy.js

const productScenarios = [
  {
    name: 'empty',
    fixture: 'products/empty.json',
    assert: () => {
      cy.get('[data-cy=products-empty]').should('be.visible');
    },
  },
  {
    name: 'basic list',
    fixture: 'products/basic-list.json',
    assert: () => {
      cy.get('[data-cy=product-card]').should('have.length', 3);
      cy.contains('[data-cy=product-card]', 'Tea')
        .find('[data-cy=product-sale-badge]')
        .should('be.visible');
    },
  },
];

describe('Products scenarios (fixtures)', () => {
  productScenarios.forEach((s) => {
    it(`renders correctly: ${s.name}`, () => {
      cy.intercept('GET', '/api/products', { fixture: s.fixture }).as('getProducts');
      cy.visit('/products');
      cy.wait('@getProducts');
      s.assert();
    });
  });
});

Now answer the exercise about the content:

Which approach is recommended to stub an API response with fixture data in Cypress while avoiding timing issues and keeping the intercept setup concise?

You are right! Congratulations, now go to the next page

You missed! Try again.

Replying with { fixture: '...' } keeps the intercept concise and avoids common timing/context pitfalls that can happen when trying to use aliased data inside intercept handlers.

Next chapter

Network Stubbing Basics with cy.intercept for Deterministic Cypress E2E Tests

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.