DEV Community

Matej Štetiar
Matej Štetiar

Posted on Originally published at jsonfabrica.com

Playwright Test Data: Seeding a Real Backend for E2E Suites

Playwright test data is the set of database rows or API records your
application needs to already contain before a browser test runs against
it — a logged-in user, their orders, the products those orders
reference — generated deterministically so the same run produces the
same data every time. Unlike unit tests, a Playwright (or Cypress) spec
drives a real browser against a real, running app, which means the
backend behind it needs real rows to serve, not an intercepted network
response. Getting that data right, and getting it there before the
first test starts, is most of what makes a browser E2E suite fast and
non-flaky instead of slow and order-dependent.

Why is E2E test data hard to manage?

Three patterns keep showing up, and each causes a different failure
mode:

  • Tests create their own data through the UI. A test that needs an order to exist first signs up a user, logs in, adds a product to a cart, and checks out — all before the actual assertion it cares about. That's slow multiplied across every spec that needs similar setup, and it means the thing under test (the UI) is also the thing doing the setup, so a bug in signup breaks fifty unrelated tests.
  • A shared, mutable test database. If every spec reads and writes the same rows, test order starts to matter: a test that deletes a user breaks a later test that assumed that user still exists. This is one of the most common sources of a suite that passes locally, one file at a time, and fails intermittently in CI when specs run in parallel or in a different order.
  • Hand-maintained fixture SQL or JSON. A fixtures.sql file or a static users.json works until the schema changes — a column gets renamed, a new required field is added — and the fixture silently stops matching what the app expects, or starts failing inserts with no clear signal about which of forty rows is the problem.

The fix for all three is the same shape: generate the data the suite
needs from a definition (a template), with a fixed seed, right before
the suite runs, and load it in directly rather than through the UI.

How do you manage Playwright test data for E2E suites?

The pattern is: describe the records your app needs as templates, fix a
seed so the same call always produces the same ids and field values,
and generate them as one relational batch so a logged-in test user
actually owns the orders your assertions expect — no separately
generated orders that need their userId patched in afterward.

{
  "seed": 20260903,
  "documents": [
    { "templateId": "tpl_user", "alias": "user", "count": 1,
      "params": { "email": "jane.doe@example.com" } },
    { "templateId": "tpl_order", "alias": "order", "count": 3,
      "relations": { "userId": { "from": "user.id", "strategy": "round-robin" } } }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Posted to the batch generation API, that
returns one user and three orders where every order.userId is that
user's generated id. Because the seed is fixed, the user's name,
email, and order details come back identical on every run, which is
what lets a spec assert something as specific as "Jane Doe has exactly
3 orders" instead of a vaguer "the orders list is non-empty." Small
batches return the records synchronously in the response; a large batch
returns a batchId you poll instead, which matters if a suite seeds
thousands of rows before a load-style E2E run.

This is a different problem from serving mock API responses. If you're
mocking network calls so a frontend can run with no backend at all, see
mock API response data instead — this
post is about the opposite case, where a real backend and database
exist and the browser test exercises them end to end. It's also
different from seeding a human developer's local database for
onboarding, covered in seeding a local dev database;
that data is long-lived and browsed by hand, while E2E test data is
short-lived, created or reset per run or per CI job, and read by
assertions that check exact values.

How do I wire test data generation into Playwright's global setup?

Call the batch API from globalSetup, before any spec file runs, and
save what comes back to a fixture file specs can import:

// playwright.config.ts
export default defineConfig({
  globalSetup: require.resolve('./e2e/global-setup.ts'),
  // ...
});
Enter fullscreen mode Exit fullscreen mode
// e2e/global-setup.ts
import fs from 'node:fs';

export default async function globalSetup() {
  await resetTestDatabase(); // your own truncate/reset code

  const res = await fetch('https://api.jsonfabrica.com/v1/batches', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.JSONFABRICA_API_KEY}`,
      'content-type': 'application/json',
    },
    body: JSON.stringify({
      seed: 20260903,
      documents: [
        { templateId: 'tpl_user', alias: 'user', count: 1 },
        {
          templateId: 'tpl_order',
          alias: 'order',
          count: 3,
          relations: { userId: { from: 'user.id', strategy: 'round-robin' } },
        },
      ],
    }),
  });
  const { results } = await res.json();

  // results is a flat array of { batchId, alias, seqNo, templateId,
  // status, result, documentSeed }; group by alias for easy lookup.
  const seeded: Record<string, unknown[]> = {};
  for (const item of results) {
    (seeded[item.alias] ??= []).push(item.result);
  }

  await loadIntoDatabase(seeded); // your own insert code, or POST to your API

  fs.writeFileSync(
    './e2e/fixtures/seeded-data.json',
    JSON.stringify(seeded, null, 2),
  );
}
Enter fullscreen mode Exit fullscreen mode

Specs then import seeded-data.json instead of re-deriving expected
values:

import seeded from '../fixtures/seeded-data.json';

test('order history shows all orders', async ({ page }) => {
  await page.goto(`/users/${seeded.user[0].id}/orders`);
  await expect(page.getByTestId('order-row')).toHaveCount(
    seeded.order.length,
  );
});
Enter fullscreen mode Exit fullscreen mode

Be clear about what's actually happening here: JsonFabrica returns JSON
records over HTTP. It doesn't insert anything into your database and
it isn't a Playwright plugin, fixture, or reporter — resetTestDatabase
and loadIntoDatabase are your own code, the same as they'd be if the
data came from a static fixture file. What changes is where the data
comes from and how it's kept consistent, not who's responsible for
getting it into the app.

The same generated payload can also back an authenticated
storageState: seed the user in globalSetup, log in via an API call
using that user's generated credentials, and save the resulting cookies
so every spec starts already authenticated instead of clicking through
a login form.

Cypress doesn't have a globalSetup hook, but the equivalent is a
before() block in a top-level spec, or a cy.task
that runs Node code outside the browser sandbox — either one making the
same HTTP call to the batch API before the suite's tests run.

How do you isolate E2E test data across parallel workers?

Running specs across multiple Playwright workers against a shared
backend means two workers can both try to draw the "next" sequential
order number or invoice ID at the same time. Namespacing avoids the
collision: pass a sequenceNamespace and variableNamespace on the
batch request, keyed to the worker index (process.env.TEST_PARALLEL_INDEX
in Playwright), so each worker gets its own counters instead of sharing
one. The mechanics of how createSeq/getSeq and namespaces produce
collision-free values are covered in
unique test data generation with sequences and counters
worth reading if your templates use sequences for order numbers, invoice
numbers, or anything else that has to be unique within a run.

Should E2E tests use a fixed seed or random data?

Both, at different times. A fixed seed is the default: it's what
makes an assertion like "the third order in the list is a $42.00
Widget" stable across a hundred CI runs, and it's required for visual
regression snapshots to stay pixel-stable. But a suite that only ever
runs against the same seed can accumulate hidden assumptions — a test
that happens to pass because generated record #2 always sorts after
record #1, not because the sort logic is actually correct. Periodically
run the suite with the seed dropped (or rotated) to let genuinely
different data flow through and catch order-dependence or
off-by-one assumptions that a fixed seed was quietly hiding.

Whichever mode you're in, schema changes are still a manual step:
adding a required field to the orders table means editing the
tpl_order template by hand, the same way you'd edit a fixture file —
generation doesn't watch your schema for you.

FAQ

How do I seed test data for Playwright tests?
Call a batch generation API from playwright.config.ts's globalSetup,
before any spec runs, and write the created records (with their
generated ids) to a JSON fixture file that your specs import. Reset the
backend to a known empty or baseline state first, then insert the
generated batch, so every run starts from the same data.

Should Playwright tests create their own data through the UI?
Not as the default pattern. Clicking through signup and creation flows
to set up state for every test is slow, and it makes tests
order-dependent when they share a database, which is a common source of
flaky suites. Reserve UI-driven creation for the handful of tests
actually verifying that creation flow, and seed everything else
directly through an API before the suite starts.

What is Playwright global setup used for?
globalSetup is a function Playwright runs once before any test file,
configured via the globalSetup option in playwright.config.ts. It's
the standard place to seed a backend's database, generate an
authenticated storageState, or do any other one-time preparation the
whole suite depends on.

How do you keep parallel E2E test workers from colliding on data?
Give each parallel worker its own namespace when generating data, so
counters and any workflow-scoped state don't overlap between workers
hitting the same backend at once. JsonFabrica's batch API exposes this
as a sequenceNamespace and variableNamespace per request, keyed on
something like the worker index.

Does JsonFabrica integrate directly with Playwright or Cypress?
No. There's no official plugin, fixture, or reporter for either tool.
JsonFabrica is an HTTP API that returns JSON records; you call it from
your own globalSetup, before() hook, or cy.task, and you write the
code that loads the returned records into your application's database
or API.

Should E2E tests use a fixed seed or random data?
Use a fixed seed most of the time, because it makes generated ids and
field values identical across runs, which is what lets an assertion
like "Jane Doe has exactly 3 orders" stay true instead of drifting.
Periodically run the suite with the seed dropped so genuinely random
data can surface assertions that were quietly depending on a specific
generated value instead of the behavior you meant to test.

Deterministic, relational test data is what the batch generation
API
is built for — seed it once in your
globalSetup, and every Playwright or Cypress run gets the same known
data to assert against.

Top comments (0)