← All guides

Deterministic test data: reset your mock API between Playwright & Cypress tests

The classic end-to-end testing failure mode: test A creates a record, test B assumes it isn't there, and your suite passes or fails depending on run order. The usual fixes all hurt β€” truncate-and-reseed scripts against a real database (slow, needs DB access from CI), spinning up throwaway containers per run (slow, infra-heavy), or mocking every request in the browser with MSW (fast, but now your fixtures live in handler code and drift from reality).

If your frontend talks to a Mockbird project, there's a simpler shape: the mock API itself supports named snapshots of its entire data state. Save a baseline once; restore it in one HTTP call before each test (or each run). Every record in every resource comes back exactly as saved β€” same ids, same values, deleted resources rebuilt.

1. Create a project and save a baseline

One request creates a seeded multi-resource backend (no signup needed to try it):

curl -X POST https://HOST/api/projects \
  -H 'content-type: application/json' \
  -d '{"name": "e2e fixtures", "preset": "saas"}'

The response has your project id and adminKey. Arrange the data how your tests expect it β€” seed presets, import a db.json, or hand-edit records in the dashboard β€” then freeze it:

curl -X POST https://HOST/api/projects/<id>/snapshots \
  -H 'content-type: application/json' -H 'x-admin-key: KEY' \
  -d '{"name":"baseline"}'
# β†’ {"ok":true,"name":"baseline","resources":4,"records":111,...}

2. Restore it before every test

Restoring is a single POST β€” no body needed:

curl -X POST https://HOST/api/projects/<id>/snapshots/baseline/restore \
  -H 'x-admin-key: KEY'
# β†’ {"ok":true,"restored":"baseline","records":111,...}

Playwright β€” restore in beforeEach (or once per worker in a fixture if your tests don't write):

// tests/helpers.ts
export async function restoreBaseline() {
  const res = await fetch(
    `https://HOST/api/projects/${process.env.MOCK_PROJECT}/snapshots/baseline/restore`,
    { method: "POST", headers: { "x-admin-key": process.env.MOCK_ADMIN_KEY! } }
  );
  if (!res.ok) throw new Error(`restore failed: ${res.status}`);
}

// tests/todos.spec.ts
import { test, expect } from "@playwright/test";
import { restoreBaseline } from "./helpers";

test.beforeEach(async () => { await restoreBaseline(); });

test("deleting a todo removes it from the list", async ({ page }) => {
  await page.goto("/todos");
  // mutate freely β€” the next test starts from baseline anyway
});

Cypress β€” same idea with cy.request:

beforeEach(() => {
  cy.request({
    method: "POST",
    url: `https://HOST/api/projects/${Cypress.env("MOCK_PROJECT")}/snapshots/baseline/restore`,
    headers: { "x-admin-key": Cypress.env("MOCK_ADMIN_KEY") },
  });
});

Keep MOCK_ADMIN_KEY in a CI secret, not in the repo β€” it's the write key to the whole project.

3. One snapshot per scenario

You get 10 named snapshots per project, so keep one per state your UI cares about:

SnapshotWhat it holdsTests that use it
baselinethe normal seeded datasetmost specs
emptyall resources, zero recordsempty states, onboarding, "create your first…" flows
edge-casesunicode names, 0-price products, very long stringsrendering & validation specs
bug-1234the exact dataset that reproduces a bug reportthe regression test for it
test.describe("empty states", () => {
  test.beforeEach(() => restoreSnapshot("empty"));
  // ...
});

To build an empty snapshot: wipe each resource with a zero-count reseed, save as empty, then restore baseline to get your data back:

# wipe (repeat per resource), snapshot, un-wipe
curl -X POST https://HOST/api/projects/<id>/resources/todos \
  -H 'content-type: application/json' -H 'x-admin-key: KEY' -d '{"seed":0}'
curl -X POST https://HOST/api/projects/<id>/snapshots \
  -H 'content-type: application/json' -H 'x-admin-key: KEY' -d '{"name":"empty"}'
curl -X POST https://HOST/api/projects/<id>/snapshots/baseline/restore -H 'x-admin-key: KEY'

Saving with an existing name overwrites, so updating a fixture is just: arrange data β†’ save again.

4. Parallel workers: pin a snapshot per request, no restore at all

Restore has one weakness: it mutates shared state. If Playwright runs 4 workers against one project, worker A's restore("empty") yanks the data out from under worker B mid-spec. The usual fixes β€” one project per worker, or workers: 1 β€” cost you setup code or speed.

Mockbird has a third option: any GET (or GraphQL query) can be answered directly from a named snapshot, per-request, without touching live data. Send the X-Mockbird-Snapshot header (or ?mock_snapshot= where you can't set headers):

# live data
curl https://HOST/m/<id>/todos

# same URL, served from the "empty" snapshot β€” live data untouched
curl https://HOST/m/<id>/todos -H 'X-Mockbird-Snapshot: empty'

In Playwright, pin a scenario for a whole spec file by injecting the header at the browser level β€” your app code doesn't change:

test.use({ extraHTTPHeaders: { "X-Mockbird-Snapshot": "edge-cases" } });

test("renders unicode names without clipping", async ({ page }) => {
  await page.goto("/products");   // every API call now sees the edge-cases data
  // ...
});

Or per-request with page.route if only some calls should be pinned. Filters, sorting, pagination, _expand/_embed and nested routes all work against the snapshot's records. Snapshot mode is read-only β€” writes return 405 β€” so specs that mutate data still want the restore pattern from Β§2; but read-mostly UI specs can run fully parallel, one scenario per worker, on a single project with zero interference.

Try it in 5 seconds, no setup β€” the public demo ships with two built-in snapshots, empty and edge-cases (100-char product name, unicode + HTML-ish characters, price 0 and 1999999.99, empty description, out-of-stock one-star item, non-sequential id 9999):

curl "https://HOST/m/demo/products?mock_snapshot=empty"        # β†’ []
curl "https://HOST/m/demo/products?mock_snapshot=edge-cases"   # β†’ 7 deliberately nasty records
curl https://HOST/m/demo/products -H 'X-Mockbird-Snapshot: edge-cases'   # header variant

What restore guarantees

Compared to the alternatives

ApproachReset speedWorks in CI/preview deploysFixture lives in
DB truncate + reseed scriptseconds, needs DB accessonly with DB creds wired into CISQL/ORM scripts
Fresh containers per runtens of secondsneeds Docker in CImigration + seed code
MSW / request interceptioninstantyes (in-browser)handler code you maintain
Mockbird snapshotsone HTTP callyes β€” it's a hosted URLthe mock API itself, editable in a UI

Honest scope note: Mockbird mocks your backend, so this is for testing frontends (or API clients) against realistic data β€” it doesn't reset state inside your real backend. Restores share the management API; mock traffic from your tests counts against the project's 10,000 requests/day cap, which is roomy for a CI suite but worth knowing. Everything is free while Mockbird is in beta.

Snapshots pair well with simulated latency and error responses β€” deterministic data and deterministic failure modes, from one URL. Create a project β†’