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://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/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. (Or sidestep the secret entirely: create an ephemeral project per CI run β the key is born inside the run, masked, and deleted with the project in an always() step.)
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://mockbird.mockbird.workers.dev/api/projects/<id>/resources/todos \
-H 'content-type: application/json' -H 'x-admin-key: KEY' -d '{"seed":0}'
curl -X POST https://mockbird.mockbird.workers.dev/api/projects/<id>/snapshots \
-H 'content-type: application/json' -H 'x-admin-key: KEY' -d '{"name":"empty"}'
curl -X POST https://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/m/<id>/todos
# same URL, served from the "empty" snapshot β live data untouched
curl https://mockbird.mockbird.workers.dev/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.
The same header trick fixes Vitest suites too β Vitest runs test files in parallel by default, so shared mutable fixtures race; pin each file to its own snapshot instead. Worked examples in the Vitest and Jest guides (Jest's parallel worker processes race the same way).
Playwright-first team? The full Playwright treatment β request-fixture specs, cross-host page.route rewrites, the route.fallback composition gotcha, and per-worker snapshot pinning β is in the Playwright guide (every snippet run verbatim).
Testing Python instead? The same pinning works as a pytest fixture β a requests.Session with the header set, parametrized per scenario. Full example (run before publishing) in the Python guide.
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://mockbird.mockbird.workers.dev/m/demo/products?mock_snapshot=empty" # β []
curl "https://mockbird.mockbird.workers.dev/m/demo/products?mock_snapshot=edge-cases" # β 7 deliberately nasty records
curl https://mockbird.mockbird.workers.dev/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. The same snapshot-per-episode loop also makes a clean eval sandbox for AI agents. Create a project β