Playwright test data

Fixtures that are identical on your laptop and in CI, so a red test means the application changed โ€” not that the data did.

Generate JSON fixture โ†’QA test data guide

Why generated beats faked-at-runtime

Calling a fake-data library inside a test gives different values on every run. That sounds like better coverage and behaves like a flaky suite: a test fails once in forty runs, nobody can reproduce it, and eventually somebody adds a retry. Generating the data once with a seed and committing the file moves the randomness to a place you control โ€” you still get realistic, varied data, but the same realistic, varied data every time.

When you do want a different dataset, change the seed deliberately and commit that. The variation becomes a decision with a diff attached rather than a background process.

A committed fixture

Export JSON with a seed and save it under tests/fixtures/:

// tests/fixtures/users.json โ€” seed "users-v3", 25 rows
import users from './fixtures/users.json' with { type: 'json' };
import { test, expect } from '@playwright/test';

test('user list renders every row', async ({ page }) => {
  await page.goto('/users');
  await expect(page.getByTestId('user-row')).toHaveCount(users.length);
  await expect(page.getByText(users[0].full_name)).toBeVisible();
});

Because the seed fixes the output, users[0].full_name is a known value. The assertion reads clearly and the failure message names an actual person instead of "expected 25, received 24".

Data-driven tests

Generate the edge cases as their own small fixture and loop โ€” Playwright creates a separate test per entry, so one bad row fails one test rather than the whole block:

import cases from './fixtures/signup-cases.json' with { type: 'json' };

for (const c of cases) {
  test(`signup validation: ${c.label}`, async ({ page }) => {
    await page.goto('/signup');
    await page.getByTestId('email').fill(c.email);
    await page.getByTestId('submit').click();
    await expect(page.getByTestId('form-error')).toHaveText(c.expected);
  });
}

Build that fixture from a Custom List field holding the malformed inputs you care about, alongside a generated column of valid ones.

Mocking the API instead of the database

For a frontend-only run, serve the fixture straight from page.route โ€” no backend, no database, and full control over the response states that are awkward to trigger for real:

await page.route('**/api/users*', route =>
  route.fulfill({ status: 200, json: users })
);

// the states a real backend rarely produces on demand
await page.route('**/api/users*', route => route.fulfill({ status: 500 }));
await page.route('**/api/users*', route => route.fulfill({ status: 200, json: [] }));
await page.route('**/api/users*', route => route.abort('failed'));

The empty array is the one worth writing first. Empty states are the most commonly broken screen in any application, precisely because the development database is never empty.

Seeding a real database

For full end-to-end runs, export SQL and load it in globalSetup so every worker starts from the same known state:

// playwright.config.ts โ†’ globalSetup: './tests/seed.ts'
import { execSync } from 'node:child_process';
export default async function seed() {
  execSync('psql $TEST_DB -f tests/fixtures/seed.sql');
}

Reseeding between runs rather than accumulating rows is what keeps a suite honest โ€” a test that only passes on the third run is a test that depends on leftovers.

Practising against a real site

This generator is itself built for automation practice: every interactive control carries a stable data-testid, and those names do not change between deploys. Point a Playwright script at the builder, fill the schema form, trigger an export and assert on the preview โ€” it exercises forms, selects, dialogs, drag-and-drop reordering, downloads and dynamically rendered rows without needing a test environment of your own.

Common questions

Should I generate data at runtime or commit a fixture?

Commit a fixture generated with a seed. Runtime fake data varies per run, which turns a real failure into an unreproducible flake. A seeded file gives you realistic variety that is identical everywhere.

How do I write data-driven Playwright tests?

Export a small JSON fixture of cases and loop over it, calling test() inside the loop. Playwright registers one test per entry, so a single bad case fails on its own rather than taking the block with it.

Can I use fixtures without a backend?

Yes. page.route with route.fulfill serves the JSON directly, which also makes error, empty and timeout states trivial to test compared with provoking them from a real server.

How do I seed a database before an end-to-end run?

Export SQL and load it from globalSetup in playwright.config.ts so every worker starts from the same state. Reseed between runs instead of accumulating rows.

Does this site work as a Playwright practice target?

Yes. Every control has a stable data-testid that is kept unchanged across deploys, covering forms, dialogs, drag-and-drop, downloads and dynamic rows.

Related