On this page
- What actually counts as synthetic data
- Seeded generation versus committed fixture files
- Reserved ranges that cannot collide with reality
- Referential integrity across generated entities
- What this site refuses to generate
- Volume, determinism, and performance
- Keeping synthetic data out of real pipelines
- A pre-use review checklist
- Questions
Synthetic test data is data that was manufactured rather than collected. It describes nobody, it can be regenerated on demand, and it never has to be masked, purged, or subpoenaed. For teams that test address forms, checkout flows, or import pipelines, it is the only kind of data that can be used freely in CI, screenshots, and demos.
The catch is that synthetic data is only useful when it is both plausible and safe. Implausible data misses the bugs. Unsafe data creates them. This guide covers the middle path: seeded generation for reproducibility, reserved value ranges that cannot collide with reality, referential integrity across entities, and the specific identifiers a generator should refuse to produce.
What actually counts as synthetic data
Synthetic data is generated from rules, not sampled from people. A name assembled from a name list, an address assembled from a country fixture, a phone number from a reserved block, an email on a reserved domain: each is manufactured and none refers to a real subject. Reproducibility is what separates it from random noise, because unseeded output cannot be reviewed, diffed, or replayed.
Anonymized production data is not synthetic. Masking a real row leaves the same distribution and often the same re-identification risk, and a masked EMAIL column that keeps the domain still leaks the employer. If a test needs production-shaped data, that is a separate conversation about aggregation and access, not something a generator can honestly provide.
- Generated from rules, not sampled from people
- Reproducible from a seed
- Reviewable: a diff tells you what changed
- Never derived from a production row
Seeded generation versus committed fixture files
A committed fixture file has one advantage: it is easy to inspect. It has several problems. It drifts, because nobody updates it when the schema changes. It bloats the repository. It gets copied between projects until nobody knows which version is current. And it quietly becomes the only data anyone tests against, so the suite covers exactly the values someone typed in 2021.
Seeded generation keeps the input tiny and the output large. The seed plus the generator version is the fixture. Regenerate on demand, snapshot only the first and last record for review, and treat any change in generator output as a deliberate change that requires a re-baseline. The one requirement is that the generator must be pinned, because an upgraded generator with a changed algorithm is a new data set even at the same seed.
| Approach | Repo cost | Reproducible | Drift risk |
|---|---|---|---|
| Committed JSON fixtures | High | Yes, but frozen | Rises with age |
| Seeded generator at test time | Low | Yes, with a pinned version | Low |
| Unseeded random data | Low | No | High, and flaky |
| Masked production dump | High | No | High, plus privacy risk |
Reserved ranges that cannot collide with reality
Several standards reserve values for documentation and testing. Using them is not a style preference; it is the difference between a fixture that is guaranteed harmless and one that might belong to someone. The reserved blocks are stable, widely documented, and accepted by most validators as syntactically normal.
For email, use a domain under .test, such as example.test, which is reserved and cannot be registered. For phone numbers in the North American Numbering Plan, the 555-0100 through 555-0199 block is reserved for fictional use. For IP addresses, the documentation ranges are 192.0.2.0/24, 198.51.100.0/24, and 203.0.113.0/24 in IPv4 and 2001:db8::/32 in IPv6. For MAC addresses, the documentation range is 00-00-5E-00-53-00 through 00-00-5E-00-53-FF, and locally administered addresses with the U/L bit set are also safe for fixtures.
| Value type | Reserved for testing | Authority |
|---|---|---|
| Email domain | example.test, example.com, example.org | RFC 2606 and RFC 6761 |
| Web domain | .test, .invalid, .localhost | RFC 2606 |
| US and Canada phone | NANP 555-0100 to 555-0199 | NANP fiction range |
| IPv4 documentation | 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24 | RFC 5737 |
| IPv6 documentation | 2001:db8::/32 | RFC 3849 |
| MAC documentation | 00-00-5E-00-53-xx | RFC 7042 |
| Locally administered MAC | First octet 0x02, I/G bit clear | IEEE 802 U/L bit |
Referential integrity across generated entities
Most systems need more than one entity. An order has a customer, a shipping address, and a billing address, and the three must agree in ways the schema enforces. If the billing address is in the United States and the shipping address is in Japan, the tax calculation and the carrier selection have to accept that combination, or the test fails for a reason unrelated to the behaviour under test.
Generate relationships, not rows. Derive child seeds from the parent seed so the order and its addresses are reproducible as a unit. Decide deliberately which fields must agree: country between shipping and billing is usually the same, postal code must match the city in the fixture, currency must match the country. Encode those constraints in the factory so a broken fixture fails at generation time rather than in an assertion three layers down.
- Derive child seeds from the parent seed, not from the clock
- Keep country consistent across billing, shipping, and tax
- Keep postal code consistent with the city fixture
- Keep currency consistent with the country
- Fail at generation time when a constraint is violated
What this site refuses to generate
The site does not generate Social Security numbers, payment card numbers, IMEI or IMSI values, Bluetooth device addresses, or serial numbers. Each refusal has a reason. SSNs are legally restricted identifiers in the United States. Card numbers carry a Luhn checksum, so a plausible one can pass a naive validator and then be pasted into a support ticket. IMEIs have a check digit and an allocation registry. Bluetooth addresses share the MAC format but carry no vendor meaning, and a generated one invites exactly the confusion the MAC tools are designed to avoid.
The same reasoning applies to any identifier with a checksum or a registry. If a test genuinely needs one, use the officially published test values from the processor or the issuing authority, not a generator. That keeps the value obviously fake to anyone who recognizes the test pattern, and it keeps your screenshot out of a compliance review.
| Identifier | Generated here | Correct alternative |
|---|---|---|
| Social Security number | No | Never use a real or realistic SSN |
| Payment card number | No | Processor test cards for the sandbox |
| IMEI or IMSI | No | Vendor test values, if one exists |
| Bluetooth address | No | Not needed for address form tests |
| Device serial number | No | An opaque fixture id you control |
| Email address | Yes, example.test only | Safe, non-resolving domain |
| Phone number | Yes, NANP 555-0100 to 555-0199 | Reserved fiction range |
Volume, determinism, and performance
Generating a hundred records at test time is cheap. Generating ten million is not, and doing it inside a test setup will dominate the runtime. Precompute large volumes once into an isolated store, with the seed recorded in the manifest, and keep the generator out of the hot path. For medium volumes, generate in a single batch rather than per row, because per-row setup is usually the actual cost.
Determinism has a performance dimension too. Parallel workers must not share a single sequential generator, because the interleaving changes which worker gets which record. Give each worker its own seed derived from the run seed and the worker index, so the dataset is stable regardless of scheduling. That also makes a failure on worker three reproducible without rerunning the whole suite.
- Small volumes: generate inline, seeded
- Large volumes: precompute to an isolated store with a manifest
- Parallel workers: derive a seed per worker index
- Never share a single generator instance across threads
- Record the generator version with the seed
Keeping synthetic data out of real pipelines
The hazard of good synthetic data is that it looks real. Put a marker on every generated record: a fixture flag, a dedicated tenant, an email on example.test, a phone in the 555-01xx range. Block those records from sending mail, capturing payments, or entering analytics. A demo customer that reaches a production CRM is a support incident waiting for a salesperson to notice.
Also keep generated data out of logs that are retained for compliance, and out of screenshots that will be published. Even though the values describe nobody, a reader cannot tell that from the image, and explaining it later is more expensive than labeling it now. If a screenshot must show a realistic address, use a documentation-range value and add a visible "sample data" caption.
- Tag every generated row with a fixture flag or tenant
- Block fixture tenants from email, payments, and analytics
- Use example.test and 555-01xx, never a plausible real domain
- Caption screenshots that show generated values
- Purge fixtures from shared staging on a schedule
A pre-use review checklist
Before a generated dataset is used in a shared environment, four questions need answers. Where did it come from, and can it be regenerated? Is every value in a reserved or locally administered range? Does it satisfy the schema constraints and the referential rules? And is it labeled so that a human reader cannot mistake it for a customer?
If any answer is no, the dataset is not ready. This is deliberately strict, because the cost of a wrong answer is asymmetric: regenerating takes minutes, while a synthetic customer in a revenue report can distort decisions for a quarter. Run the checklist as part of the review, not as a comment in a wiki.
| Question | Pass condition | Evidence |
|---|---|---|
| Origin | Seed and generator version recorded | Manifest file |
| Safety | All values in reserved or local ranges | Validator run on the dataset |
| Integrity | Schema and cross-entity rules satisfied | Factory tests green |
| Labeling | Fixture flag on every row | Query returns zero unlabeled rows |
Questions
- Yes, when every value is synthetic and reserved, for example example.test emails and NANP 555-01xx phones. Prefer regenerating from a seed over committing large datasets.
- Those identifiers are regulated or checksummed, and a realistic fake can pass naive validation and end up in a screenshot or a ticket. Use official test values from the processor or authority instead.
- Derive a seed per worker from the run seed and the worker index. A single shared generator produces a different sequence depending on scheduling.
- No. Masking keeps the original distribution and often the re-identification risk. Synthetic data is generated from rules and describes no real subject.
- 38 countries with five city fixtures each. Hong Kong and the United Arab Emirates return no postal code, because neither has one.
Is synthetic test data safe to commit?+
Why does the generator refuse to produce card or SSN values?+
How do I keep parallel tests reproducible?+
Is masked production data the same thing?+
How many countries does the synthetic address data cover?+
Examples are synthetic. Do not use a generated address for identity checks, account registration, or delivery.