Using Fake JSON Data in Cypress and Playwright E2E Tests

June 11, 2026

End-to-end tests that rely on a real database are brittle — they break when data changes, run slowly, and can't be parallelised safely. The better approach: intercept API calls and return controlled fake JSON data. This guide shows exactly how to do that in both Cypress and Playwright.


Why Intercept Instead of Using a Real Database?

  • Speed — no database queries, no network round trips. Tests run 3–10x faster.
  • Reliability — your test data never changes unexpectedly between runs.
  • Isolation — parallel test runs can't step on each other's data.
  • Edge cases — you control exactly what the API returns, including error states, empty states, and unusual data combinations.

Cypress: cy.intercept() with Fake JSON

Basic Intercept

Generate your test data with Dummy JSON Generator and save it as a fixture file in cypress/fixtures/.

// cypress/fixtures/users.json  ← your generated file
[
    { "id": 1, "fullName": "Ayesha Rahman", "email": "ayesha@example.com", "status": "active" },
    { "id": 2, "fullName": "James O'Brien", "email": "james@example.com", "status": "inactive" }
]
// cypress/e2e/users.cy.ts
describe('Users page', () => {
    beforeEach(() => {
        cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers');
    });

    it('displays user list', () => {
        cy.visit('/users');
        cy.wait('@getUsers');
        cy.get('[data-testid="user-row"]').should('have.length', 2);
    });

    it('shows user emails', () => {
        cy.visit('/users');
        cy.wait('@getUsers');
        cy.contains('ayesha@example.com').should('be.visible');
    });
});

Testing Empty States

it('shows empty state when no users exist', () => {
    cy.intercept('GET', '/api/users', { body: [] }).as('getEmptyUsers');
    cy.visit('/users');
    cy.wait('@getEmptyUsers');
    cy.get('[data-testid="empty-state"]').should('be.visible');
    cy.contains('No users found').should('be.visible');
});

Testing Loading and Error States

it('shows error state on API failure', () => {
    cy.intercept('GET', '/api/users', {
        statusCode: 500,
        body: { error: 'Internal server error' }
    }).as('failedUsers');
    cy.visit('/users');
    cy.wait('@failedUsers');
    cy.get('[data-testid="error-message"]').should('be.visible');
});

it('shows loading spinner while fetching', () => {
    cy.intercept('GET', '/api/users', (req) => {
        req.reply((res) => {
            res.setDelay(1000); // 1 second delay
            res.send({ fixture: 'users.json' });
        });
    });
    cy.visit('/users');
    cy.get('[data-testid="loading-spinner"]').should('be.visible');
});

Intercepting Dynamic Route Params

cy.intercept('GET', '/api/users/*', { fixture: 'user-single.json' }).as('getUser');
cy.intercept('GET', '/api/users/*/orders', { fixture: 'orders.json' }).as('getUserOrders');

Playwright: route.fulfill() with Fake JSON

Basic Route Interception

// tests/fixtures/users.json  ← your generated file
// tests/users.spec.ts
import { test, expect } from '@playwright/test';
import users from './fixtures/users.json';

test.describe('Users page', () => {
    test.beforeEach(async ({ page }) => {
        await page.route('**/api/users', async route => {
            await route.fulfill({
                status: 200,
                contentType: 'application/json',
                body: JSON.stringify(users),
            });
        });
    });

    test('displays user list', async ({ page }) => {
        await page.goto('/users');
        const rows = page.locator('[data-testid="user-row"]');
        await expect(rows).toHaveCount(users.length);
    });

    test('shows user email', async ({ page }) => {
        await page.goto('/users');
        await expect(page.getByText('ayesha@example.com')).toBeVisible();
    });
});

Testing Error States in Playwright

test('shows error message on 500', async ({ page }) => {
    await page.route('**/api/users', route => route.fulfill({
        status: 500,
        body: JSON.stringify({ message: 'Server error' }),
    }));
    await page.goto('/users');
    await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
});

test('shows empty state', async ({ page }) => {
    await page.route('**/api/users', route => route.fulfill({
        status: 200,
        body: JSON.stringify([]),
    }));
    await page.goto('/users');
    await expect(page.getByText('No users found')).toBeVisible();
});

Reusable Fixtures in Playwright

// tests/fixtures/index.ts
import { test as base } from '@playwright/test';
import users from './users.json';
import products from './products.json';

type Fixtures = {
    mockUsers: typeof users;
    mockProducts: typeof products;
};

export const test = base.extend<Fixtures>({
    mockUsers: async ({ page }, use) => {
        await page.route('**/api/users', route =>
            route.fulfill({ body: JSON.stringify(users) })
        );
        await use(users);
    },
    mockProducts: async ({ page }, use) => {
        await page.route('**/api/products', route =>
            route.fulfill({ body: JSON.stringify(products) })
        );
        await use(products);
    },
});

// Usage in tests:
// import { test } from './fixtures';
// test('shows users', async ({ page, mockUsers }) => { ... });

Recommended Fixture Folder Structure

cypress/fixtures/         (Cypress)
├── users.json            ← 50 users
├── user-single.json      ← 1 user
├── users-empty.json      ← []
├── products.json         ← 20 products
└── orders.json           ← 30 orders

tests/fixtures/           (Playwright)
├── users.json
├── products.json
└── orders.json

Generate each fixture file with Dummy JSON Generator — configure your schema once, then download at different record counts for different test scenarios.


Cypress vs Playwright: Which to Use?

FeatureCypressPlaywright
API mocking syntaxcy.intercept()page.route()
Fixture filesBuilt-in cy.fixture()Manual JSON import
Multi-browserChrome, Firefox, EdgeChrome, Firefox, Safari, Edge
Parallel testsPaid tierFree, built-in

Both work excellently for fake-data-driven E2E tests. Choose based on your team's existing tooling — the interception patterns are similar enough that switching later isn't painful.