Generating Realistic Fake Data with Faker

Hard-coded test data lies. When every test user is test@example.com with name Test User, a suite never exercises the messy reality of unicode names, plus-addressed emails, or the uniqueness constraints that real data trips over — and worse, it can pass against data that could never occur in production. @faker-js/faker fixes the realism problem by generating plausible names, emails, addresses, and more, but it introduces a new hazard: randomness, which is the enemy of a reproducible suite. This guide is for engineers using Vitest 3.x in TypeScript who want realistic generated data without importing flakiness. The resolution is simple and non-negotiable — seed faker’s pseudo-random generator so “random” becomes “the same sequence every run” — and this guide shows how to wire that seeding into factories, scale it to bulk data, and avoid the traps that make faker-backed suites intermittently red. It builds directly on deterministic seeding for test data in Vitest.

Root Cause Analysis

Faker is a pseudo-random generator: given a seed, it produces a fixed, repeatable sequence; given no seed, it draws from an unpredictable starting point so every run differs. Unseeded faker in tests is the textbook recipe for a Heisenbug — a test that asserts a generated email matches a pattern passes ten thousand times and fails on the ten-thousand-and-first when faker happens to produce an edge-case value the assertion did not anticipate. The failure is real (the assertion was too narrow) but it surfaces nondeterministically, so it looks like flakiness and gets “fixed” with a retry, hiding the actual weakness. The root cause is not faker; it is using randomness without pinning it.

Seeding converts faker from a source of entropy into a source of reproducible variety. With faker.seed(1234) set before generation, the same records come out on every machine and every CI run, so a failure is always reproducible and a passing run is trustworthy. This does not sacrifice realism — the values are still varied and plausible — it only removes the unpredictability. The subtle part is scope: the seed must be set at a boundary the whole suite shares (per file, in setup) rather than per assertion, because reseeding mid-file resets the sequence and couples tests through the generator’s internal counter. Get the scope right and faker gives you the best of both worlds; get it wrong and you trade one nondeterminism for another, exactly the situation a disciplined test pyramid strategy works to eliminate at every tier.

Seeded versus unseeded faker output across runs Unseeded faker produces a different sequence each run causing intermittent failures, while a fixed seed produces the identical sequence every run so failures are reproducible. Unseeded — sequence differs every run run 1: Ada run 2: Kai run 3: Zoë edge case slips through intermittent red Seeded — identical sequence every run run 1: Ada run 2: Ada run 3: Ada reproducible, trustworthy green means green
A fixed seed turns faker's randomness into reproducible variety.

Reproducible Setup

Install faker and add a setup file that seeds it before every test file runs.

npm install -D vitest @faker-js/faker
// tests/setup/faker.ts — seed once per test file for reproducibility
import { beforeEach } from 'vitest';
import { faker } from '@faker-js/faker';

beforeEach(() => {
  faker.seed(1234);      // fixed sequence
  faker.setDefaultRefDate('2026-07-24T00:00:00Z'); // stable relative dates
});
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: { setupFiles: ['./tests/setup/faker.ts'] },
});

Implementation

Step 1 — Feed factory leaves from faker, keep the factory as the single construction point. The factory still owns the shape and the overrides; faker only supplies the leaf values. Because the seed is fixed in setup, each call in a file draws the next deterministic value.

// factories/user.ts
import { faker } from '@faker-js/faker';
import type { User } from '../src/types';

export function makeUser(overrides: Partial<User> = {}): User {
  return {
    id: faker.string.uuid(),
    email: faker.internet.email().toLowerCase(),
    role: 'member',
    createdAt: faker.date.past({ years: 1 }),
    ...overrides,
  };
}

Step 2 — Generate bulk data for volume-sensitive assertions. Some tests need hundreds of rows — pagination, aggregation, sorting. Generate them in a loop from the factory so the volume is realistic and still reproducible.

// factories/bulk.ts
import { makeUser } from './user';
import type { User } from '../src/types';

export function makeManyUsers(count: number): User[] {
  return Array.from({ length: count }, () => makeUser());
}

Step 3 — Localise data when locale matters. If the code under test formats names or addresses per locale, generate from a matching faker locale so tests cover the character sets and formats users actually submit.

// factories/localized.ts
import { Faker, de, en } from '@faker-js/faker';

const deFaker = new Faker({ locale: [de, en] });
deFaker.seed(1234);

export function makeGermanAddress() {
  return {
    city: deFaker.location.city(),
    zip: deFaker.location.zipCode(),
    street: deFaker.location.streetAddress(),
  };
}

Step 4 — Pin a per-test seed only when a test asserts on a specific generated value. Most tests should assert on shape, not on an exact faker value. When a test genuinely needs “the exact email faker produces”, set the seed inside that test so it is self-contained and does not depend on how many values earlier tests consumed.

// tests/exact.test.ts
import { it, expect } from 'vitest';
import { faker } from '@faker-js/faker';
import { makeUser } from '../factories/user';

it('produces a stable email for this fixed seed', () => {
  faker.seed(42);              // local, self-contained
  const user = makeUser();
  expect(user.email).toBe(user.email.toLowerCase()); // assert on property, not literal
  expect(user.email).toContain('@');
});

Step 5 — Combine faker output with database seeding for realistic integration baselines. When seeding a test database for integration tests, generate the baseline rows with faker so joins, sorts, and uniqueness constraints face plausible data rather than a hundred identical strings.

// tests/seed-realistic.ts
import { pool } from '../src/db';
import { makeManyUsers } from '../factories/bulk';

export async function seedRealistic(): Promise<void> {
  for (const u of makeManyUsers(200)) {
    await pool.query(
      'INSERT INTO users (id,email,role,created_at) VALUES ($1,$2,$3,$4)',
      [u.id, u.email, u.role, u.createdAt],
    );
  }
}
Seed to factory to realistic dataset pipeline A fixed seed drives faker, faker supplies leaf values to the factory, and the factory yields a reproducible realistic dataset for tests or database seeding. faker.seed() fixed entropy faker.*() plausible leaves makeUser() owns the shape dataset reproducible the seed makes every downstream value deterministic
A fixed seed flows through faker and the factory to a reproducible, realistic dataset.

Verification

Prove determinism directly: generate the same data twice with the same seed and assert equality. This is the single test that guarantees the whole faker layer is reproducible.

// tests/determinism.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { faker } from '@faker-js/faker';
import { makeManyUsers } from '../factories/bulk';

describe('faker determinism', () => {
  beforeEach(() => faker.seed(1234));

  it('same seed yields identical datasets', () => {
    faker.seed(777);
    const first = makeManyUsers(50);
    faker.seed(777);
    const second = makeManyUsers(50);
    expect(second).toEqual(first);
  });

  it('generated emails are unique across a batch', () => {
    const emails = makeManyUsers(200).map((u) => u.email);
    expect(new Set(emails).size).toBe(emails.length);
  });
});

Run npx vitest run and confirm both pass. Then run it several more times: because the seed is fixed, the results must be identical every time. If the uniqueness test ever fails, faker’s default sequence produced a collision at your volume — a signal to append a monotonic suffix to generated emails, which is itself deterministic. Run with --sequence.shuffle to confirm that per-file seeding keeps files independent of one another.

Assert on properties, not on exact generated literals Property assertions like matches-a-pattern survive seed and library changes, while asserting an exact generated value is brittle. Two ways to assert on generated data Property assertion robust — survives seed change contains @, is unique, non-empty checks the invariant, not the value Exact-literal assertion brittle — breaks on upgrade expects a specific faker output only if the seed is pinned locally
Prefer property assertions; reserve exact-literal checks for tests that pin their own seed.

Troubleshooting

Symptom: a faker-backed test fails only occasionally in CI. Diagnosis: faker is unseeded, so each run draws a different sequence and an edge-case value eventually trips a too-narrow assertion. Fix: seed faker in a setup file that runs before every test file, and loosen assertions to check properties (matches a pattern, is unique) rather than exact generated literals unless the seed is pinned in that test.

Symptom: two tests in one file interfere through faker. Diagnosis: one test called faker.seed() mid-file, resetting the shared sequence so the next test’s values shifted. Fix: seed once per file in beforeEach and only reseed inside a test that is fully self-contained about it; never reseed and then rely on a sibling’s subsequent values.

Symptom: generated emails collide at high volume and violate a unique constraint. Diagnosis: faker’s email space is finite and a large batch from one seed can repeat. Fix: make uniqueness explicit by suffixing a counter — `${faker.internet.email()}.${i}` — or use faker.helpers.unique patterns, so the value stays deterministic while guaranteeing distinctness across the batch.

FAQ

Where exactly should I call faker.seed()?

In a beforeEach inside a setup file registered via setupFiles, so every test file starts from the same known point. This gives per-file reproducibility while keeping files independent. Avoid seeding in a single global beforeAll if your files run in parallel, because the ordering guarantees differ; per-file seeding in beforeEach is the robust default. Only override it inside an individual test that must be self-contained about its exact values.

Won’t a fixed seed make my data unrealistically repetitive?

No — a seed fixes the sequence, not the value. Across a batch of two hundred records you still get two hundred varied, plausible names and emails; you simply get the same two hundred on every run. Realism comes from faker’s generators; reproducibility comes from the seed. The two are orthogonal, and seeding costs you none of the variety within a run.

Should I assert on specific faker-generated values?

Rarely. Prefer asserting on properties — that an email contains @, that a name is non-empty, that a batch is unique — because those assertions survive a seed change and express real invariants. Assert on an exact generated literal only when the test pins its own seed and the exact value is the point, and even then consider whether a property assertion would be more robust. Coupling a test to a literal faker output makes it brittle to library upgrades.

How does faker fit with database seeding and MSW?

Faker supplies the values; the other layers consume them. Generate realistic rows to seed a database so joins and constraints face plausible data, and generate realistic payloads to return when intercepting requests with MSW. Keep the seed fixed across all of them so the whole test world — database rows and mocked responses alike — is reproducible in a single run.