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.
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,...}
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.
You get 10 named snapshots per project, so keep one per state your UI cares about:
| Snapshot | What it holds | Tests that use it |
|---|---|---|
baseline | the normal seeded dataset | most specs |
empty | all resources, zero records | empty states, onboarding, "create your firstβ¦" flows |
edge-cases | unicode names, 0-price products, very long strings | rendering & validation specs |
bug-1234 | the exact dataset that reproduces a bug report | the 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.
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
openapi.json alike.| Approach | Reset speed | Works in CI/preview deploys | Fixture lives in |
|---|---|---|---|
| DB truncate + reseed script | seconds, needs DB access | only with DB creds wired into CI | SQL/ORM scripts |
| Fresh containers per run | tens of seconds | needs Docker in CI | migration + seed code |
| MSW / request interception | instant | yes (in-browser) | handler code you maintain |
| Mockbird snapshots | one HTTP call | yes β it's a hosted URL | the 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 β