Dev.to · 9 min read

Playwright Test Data: Seeding a Real Backend for E2E Suites

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", "strategy": "round-robin" } } } ] } 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'), // ... }); // 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', 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 = {}; 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), ); } 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, ); }); 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.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News