Article · Network & API Mocking for Reliable Tests

Seeding Deterministic Mock Data Across Environments

The same fixture factory that passes on your laptop can fail in CI when it leans on a live clock, a random UUID, or an unseeded faker call, because the generated values differ run to run and environment to environment. This guide shows how to make mock data reproducible: fix the faker seed, build pure factory functions, and freeze time and UUIDs so every environment produces byte-identical fixtures. It extends Environment Parity & Mock Data Management and feeds the handlers you tear down in Tearing Down MSW Service Workers Between Tests.

10 sections URL: /network-api-mocking-for-reliable-tests/environment-parity-mock-data-management/seeding-deterministic-mock-data-across-environments/
Deterministic versus nondeterministic seeding Unseeded inputs produce different fixtures per environment; a fixed seed and frozen clock produce identical fixtures everywhere. Unseeded random + now() local: data X CI: data Y Seeded + frozen seed(42)+clock identical data everywhere
Unseeded randomness and live time diverge per environment; a fixed seed plus a frozen clock yield identical fixtures everywhere.

Root cause #

Fixture generators feel deterministic but rarely are. faker without a seed pulls from a process-global PRNG whose starting state varies; Date.now(), new Date(), and crypto.randomUUID() all return run-specific values. A factory that builds a user with createdAt: new Date() and id: randomUUID() produces a different object every call. Locally that is harmless because you only see one run. Across environments it is corrosive: a snapshot recorded on macOS in your timezone fails when CI runs in UTC, and a test asserting on a generated id passes only on the machine that generated it.

The async dimension makes timing especially fragile. A factory that stamps expiresAt: Date.now() + 3600_000 encodes wall-clock time into the fixture, so a test comparing it to the app’s own clock drifts by the few milliseconds between the two reads — a tiny gap that becomes a real failure under CI load. Determinism means removing every source of ambient input: seed the randomness, freeze the clock, and stub id generation.

Step-by-step fix #

1. Fix the faker seed before generating anything #

Seed once per test (or per worker) so the sequence is reproducible.

const { faker } = require('@faker-js/faker');

beforeEach(() => {
  // Same seed -> same sequence of names/emails on every machine and CI runner.
  faker.seed(42);
});

2. Write pure factory functions #

A factory should take overrides and return a plain object with no hidden time or randomness.

// factories/user.js
const { faker } = require('@faker-js/faker');

function makeUser(overrides = {}) {
  return {
    id: faker.string.uuid(),        // deterministic once faker is seeded
    name: faker.person.fullName(),
    createdAt: '2026-01-01T00:00:00.000Z', // literal, not new Date() — no clock drift
    ...overrides
  };
}
// Trade-off: literal timestamps need updating if a test asserts on "recency", but they never flake.
module.exports = { makeUser };

3. Freeze the clock and UUID source #

Use fake timers and a stubbed id generator so any code path that still reaches for the real ones is neutralized.

beforeEach(() => {
  // Pin the clock so Date.now()/new Date() are constant across environments.
  jest.useFakeTimers().setSystemTime(new Date('2026-01-01T00:00:00Z'));
});
afterEach(() => jest.useRealTimers());

// If the app uses crypto.randomUUID(), stub it to a fixed value in setup.
jest.spyOn(crypto, 'randomUUID').mockReturnValue('00000000-0000-4000-8000-000000000000');

4. Verify reproducibility in CI #

Generate fixtures twice and assert they are identical, so nondeterminism fails fast.

test('factory output is stable', () => {
  faker.seed(42);
  const a = makeUser();
  faker.seed(42);
  const b = makeUser();
  // If this ever differs, an unseeded source of randomness has crept in.
  expect(a).toEqual(b);
});

Once your data is deterministic, keep the schema it conforms to honest with Diffing OpenAPI Fixtures Against Live Schemas.

Pitfalls #

  • Seeding faker once globally but mutating it per test. Mitigation: re-seed in beforeEach so each test starts from the same state.
  • Literal timestamps that a “last updated” assertion expects to be recent. Mitigation: freeze the clock and compute relative values from the frozen now.
  • Stubbing crypto.randomUUID but missing a library that imports its own. Mitigation: inject id generation through a factory you control.
  • Sharing one mutable fixture object across tests. Mitigation: factories return fresh objects every call.
  • Locale-dependent faker output differing by environment. Mitigation: set an explicit locale alongside the seed.

Reliability targets #

Target Goal
Fixture reproducibility 100% identical across environments
Clock-drift failures 0 per 1k runs
Snapshot churn from random data 0 unexpected diffs
Environment-specific flakiness < 0.5% of runs

Frequently Asked Questions #

Q: Is faker.seed() enough on its own? A: No. It makes faker deterministic, but Date.now(), new Date(), and crypto.randomUUID() are separate ambient sources. Freeze the clock and stub id generation too.

Q: Where should I seed — globally or per test? A: Per test, in beforeEach. A single global seed drifts as soon as one test consumes a different number of random values than another.

Q: How do I handle data that genuinely needs to look “recent”? A: Freeze the system time and derive relative values from that fixed now, so “recent” is computed against a constant rather than a live clock.