randarium
Guides

How to Generate Realistic Test Data (Without Using Production Data)

A practical workflow for generating fake data for testing: why copying production is risky, what synthetic-by-construction looks like, and how to seed it.

Why copying production data is a liability, not a shortcut

“Just grab a snapshot of prod” feels efficient. It usually isn’t, and the risk compounds the longer the habit sticks around.

Production data contains real people’s information — names, emails, addresses, sometimes payment or health details. The moment you copy it into staging, you’ve extended your compliance boundary. Staging environments routinely have weaker access controls than production: more engineers have credentials, logging is more verbose, backups are less carefully rotated, and third-party tools (error trackers, analytics, CI logs) are often wired in without the same scrutiny applied to prod. Under GDPR and similar regimes, “personal data” doesn’t stop being personal data because it moved to a lower-stakes environment.

There’s also a quieter cost: once production data is in staging, it tends to stay there, and every additional place real user data lives is another place a breach or a careless screenshot in a bug report can expose it. The fix isn’t “be more careful with prod copies” — it’s generating data that was never real in the first place.

What “clearly synthetic” actually means

Good test data isn’t just fake — it’s unambiguously fake, in a way that’s safe by construction rather than by policy. Two techniques do most of the work:

Reserved domains. example.com, example.net, and example.org are permanently reserved by RFC 2606 and will never resolve to a real mail server. The .test top-level domain is reserved outright by RFC 6761 specifically for testing. Using addresses like [email protected] means there’s no chance a stray outbound email from a misconfigured test environment lands in a real inbox.

Documentation IP ranges. RFC 5737 reserves three IPv4 blocks for documentation and examples: 192.0.2.0/24, 198.51.100.0/24, and 203.0.113.0/24; RFC 3849 does the same for IPv6 with 2001:db8::/32. Populate fixtures with addresses from these ranges and nothing will accidentally route to a real host.

The underlying principle: don’t rely on “we’ll remember this is fake data” as a control. Pick formats that are fake at the protocol level, so even a config mistake that leaks test data publicly can’t cause real-world harm.

The Fake User Dataset and Synthetic Dataset Generator tools default to exactly this kind of reserved-range, reserved-domain output.

Seeded reproducibility, and why it matters for CI

Random test data that’s different every run sounds fine until a test fails intermittently and nobody can reproduce it. A seeded generator solves this: give it the same seed, get the same dataset, every time, on every machine.

This buys you a few concrete things: deterministic fixtures, where a CI pipeline pins a seed so the dataset — and test behavior — doesn’t drift; reproducible bug reports, since “here’s the seed that produced the failing dataset” beats “it happens sometimes”; and diffable changes, where a fixed seed lets you diff old generator output against new to see exactly what changed instead of random noise masking real differences.

Seeded generation is pseudorandom, not cryptographically random — that’s a feature here, not a limitation. You want the sequence to be predictable given the seed and unpredictable otherwise, which is a different goal from generating a password or a token, where predictability of any kind is a bug.

The null-rate / duplicate-rate trick

A dataset that’s uniformly clean doesn’t test anything interesting. Real-world data has gaps, dupes, and malformed rows, and your import pipeline needs to survive them. Rather than hand-crafting a handful of bad rows, generate a dataset with a configurable null rate and duplicate rate — say, 5% of fields randomly nulled and 2% of rows duplicated with minor variation — and run your validation and deduplication logic against it.

This surfaces real bugs fast: a required-field check that silently passes on null, a dedup key that misses a duplicate with different casing, an import job that halts entirely on the first bad row instead of quarantining it. Turning the knobs up also lets you stress-test degradation — does the pipeline handle 1% nulls gracefully but fall over at 20%?

The Messy Data Generator is built around exactly this: adjustable null rates, duplicate rates, and type-corruption rates layered on top of an otherwise valid schema.

A worked workflow

Here’s a concrete sequence that covers most testing needs without touching a single real record:

  1. Define the schema. Field names, types, constraints (required vs. optional, value ranges, string formats) — even a comment block is enough; it’s the contract everything else is generated against.
  2. Generate a clean, seeded dataset. Use the Synthetic Dataset Generator with a fixed seed, using reserved domains and IP ranges for anything contact-shaped. This is your “happy path” fixture.
  3. Generate a messy variant. Same schema, run through the Messy Data Generator to inject nulls, duplicates, and type mismatches at a rate you control — this exercises validation and error-handling paths.
  4. Add edge cases deliberately. Empty strings, maximum-length strings, emoji and right-to-left text, Unicode normalization oddities, and strings that look like injection payloads but are inert in context. The Edge Case Strings tool has a curated set of exactly these — inputs that reliably break parsers, sort orders, and truncation logic.
  5. Run all three tiers through your import/validation code, and keep the seeds around so any failure is reproducible on demand.

None of this requires a single row of real user data, and all three tiers are regenerable from a seed, so your test suite doesn’t accumulate a pile of hand-maintained fixture files that quietly rot.

FAQ

Isn’t anonymized production data good enough? Anonymization is harder to get right than it looks — re-identification attacks on “anonymized” datasets are a well-documented research area, and redacting names while leaving unique combinations of other fields often isn’t as safe as it feels. Synthetic-from-scratch data sidesteps the question: there’s no real person to re-identify.

Won’t synthetic data miss weird patterns that only show up in real data? It can, which is why the “messy variant” and edge-case steps matter — they’re there specifically to cover the classes of weirdness that clean synthetic data alone would miss. If you find a real-world bug that your synthetic data didn’t catch, that’s a signal to add that pattern to your generator, not to reach for a prod snapshot.

Do I need a different seed for every test run? No — for regression tests you usually want the same seed every time, so failures are reproducible. Vary the seed deliberately (e.g., in a nightly fuzz-style job) when you specifically want broader coverage over time, and log whichever seed was used.

What if my schema has fields that need to look statistically realistic, not just structurally valid (e.g., a believable age distribution)? Layer in a distribution-aware generator for those specific fields rather than uniform random values — the Distribution Sampler shapes numeric fields to a realistic distribution (normal, skewed, etc.) instead of flat randomness.