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
beforeEachso 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.randomUUIDbut 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.