There is a special kind of frustration in a test that fails in CI, passes when you re-run the job, and then refuses to reproduce on your machine. You stare at the diff, find nothing, hit retry, and it goes green — so you shrug and merge. Nine times out of ten the culprit is not your code at all. It is the data, quietly changing between runs.
If your fixtures are freshly randomised on every run, every execution of a test is secretly a different test. Most of the random draws happen to land in the safe middle of the input space, so the suite is usually green. But every so often the dice come up with a name containing an apostrophe, a number ending in zero, or a date on a month boundary — and the one run that would have caught your bug is indistinguishable from noise, because you can never make it happen again.
Same seed, same record — always
Every generator in Real Fake Data is a pure function of its inputs, and one of those inputs is a seed. Pass the same seed and you get byte-for-byte the same record, on every machine, forever. There is no hidden clock, no process-global RNG, no ambient state — the seed fully determines the output.
GET /v1/pl/person?seed=7421 → Magdalena Wróbel · PESEL 95090265040
GET /v1/pl/person?seed=7421 → Magdalena Wróbel · PESEL 95090265040
GET /v1/pl/person?seed=7422 → a different, equally-valid person
// In a Playwright test, the fixture is seeded the same way:
const person = await realFakeData.plPerson({ seed: 7421 });
expect(person.pesel).toBe('95090265040'); // stable, every run That turns a failing test into something you can actually debug. Log the seed on failure, and a red build hands you the exact input that broke it. Paste the seed into a local run and you reproduce the failure instantly — the same name, the same number, the same date flowing through the same code path. The loop from "CI is red" to "I am looking at the bug in my debugger" collapses from an afternoon to a minute.
A pattern for whole datasets
Seeding one record is useful; seeding an entire dataset is where it pays off. Derive each record’s seed from a stable base and an index, and you get a full fixture set that is both varied and completely reproducible — regenerate it a thousand times and it is identical every time.
const BASE = 40_000;
const customers = await Promise.all(
Array.from({ length: 250 }, (_, index) =>
realFakeData.plPerson({ seed: BASE + index }),
),
);
// customer[0] is always Magdalena; customer[42] is always the same
// person too. Change BASE to get a whole new — but still stable — set. Reproducible demos, too
Seeding is not only for tests. A demo environment seeded from a fixed set of seeds looks the same every time you present it: the same customers on the dashboard, the same orders in the table, the same totals in the charts. Nobody has to explain that the numbers moved because someone re-ran the seed script this morning, and a screenshot taken today still matches the app next week.
The rule of thumb: if you would be annoyed to see the data change, seed it. It costs you one query parameter and buys you a fixture set you can trust.
Determinism is not a feature you bolt on at the end — it is a property you either have from the first draw or spend weeks chasing later. Building on seeded generators means reproducibility is the default, and "works on my machine" stops being a thing you say about your test data.