Generate Realistic Test Data with Faker
Produce a million-row dataset with believable distributions, seeded reproducibility, and referential integrity, then load it with COPY.
Task
Build a large, realistic, reproducible dataset that respects foreign keys and reflects production value distributions.
When to use this
Staging has 200 test rows that share three names and one address. Pagination, search, and indexes all behave nothing like production, so performance results are meaningless.
Prerequisites
- Faker available in your language of choice, such as @faker-js/faker or faker
- A target schema with foreign keys to satisfy
- Enough disk in the test database for the volume
Steps
1. Seed the generator for reproducibility.
import { faker } from "@faker-js/faker";
faker.seed(20261022); // identical dataset on every run
Expected output: With a fixed seed, every run produces byte-identical data, so a bug found with the dataset can be reproduced exactly.
2. Generate parents before children to satisfy foreign keys.
const customers = Array.from({ length: 50_000 }, () => ({
id: faker.string.uuid(),
name: faker.person.fullName(),
country: faker.helpers.weighted(["US", "DE", "JP", "BR"], [0.55, 0.25, 0.15, 0.05]),
}));
const orders = customers.flatMap((c) =>
Array.from({ length: faker.number.int({ min: 0, max: 12 }) }, () => ({
customerId: c.id,
total: Number(faker.commerce.price()),
placedAt: faker.date.recent({ days: 90 }),
}))
);
Expected output: Fifty thousand customers plus a realistic zero-to-twelve order skew produce roughly three hundred thousand orders with valid references.
3. Write CSVs instead of INSERT statements.
node generate.mjs --format csv > orders.csv && wc -l orders.csvExpected output: A CSV an order of magnitude smaller than SQL text, ready for the fastest load path the database has.
4. Load with COPY and verify the shape.
psql shop -c "\copy orders FROM 'orders.csv' CSV HEADER" && psql shop -c "SELECT count(*), count(DISTINCT customer_id) FROM orders;"Expected output: Row counts match the generator output, distinct customers are fifty thousand, and every foreign key resolves.
5. Assert on the distribution, not just the count.
psql shop -c "SELECT country, count(*) FROM customers GROUP BY country ORDER BY count(*) DESC;"Expected output: Country proportions approximate the 55/25/15/5 weights, proving the skew landed instead of a uniform smear.
Troubleshooting
Problem
Dates land in the future
Solution
Faker default reference times drift; pin the range with faker.date.recent or between two fixed dates relative to now.
Problem
Dataset is reproducible but unrealistic
Solution
Uniform random values break every assumption about skew; use weighted helpers for enums, and real value ranges for amounts and dates.
Problem
Load fails on foreign keys
Solution
Generate in dependency order and load parents first, or stage everything and enable constraint validation after the load.
Limitations
- Synthetic data never reproduces production correlations; anonymized samples remain better for analytics-grade testing.
- Seeding makes data identical per run; regenerate with a new seed when tests begin memorizing specific rows.
Test data with believable skew
Two hundred toy rows hide every problem that scale creates: pagination that never reaches page two, indexes the planner ignores, distributions that break group-bys. Faker with a fixed seed generates a million believable rows that reproduce exactly, and weighted helpers keep the skew realistic. Loaded through COPY, the dataset appears in minutes and finally makes performance testing meaningful.
faker.seed(20261022); // reproducible
const customers = Array.from({ length: 50_000 }, () => ({
id: faker.string.uuid(),
name: faker.person.fullName(),
country: faker.helpers.weighted(["US", "DE", "JP", "BR"], [0.55, 0.25, 0.15, 0.05]),
}));
const orders = customers.flatMap((c) =>
Array.from({ length: faker.number.int({ min: 0, max: 12 }) }, () => ({
customerId: c.id,
total: Number(faker.commerce.price()),
}))
);
The seed is the contract
A fixed seed makes the dataset a build artifact: the same rows, the same uuids, the same skew, every single time. A performance regression or a boundary bug found against it reproduces exactly on any machine, and the dataset can be versioned by its seed number in a comment or a CI variable.
Shape matters more than volume
A million uniformly random rows are still toy data. Real tables are skewed: a few customers with many orders, most with none; a handful of countries dominating; totals clustered under a round number. Weighted generation preserves those shapes, which is what makes the planner, the indexes, and the queries behave like production.