Randomly

Test Data

PII-safe test data: fixtures that do not leak real people

Randomly 8 min
PII-safe test data: fixtures that do not leak real people
On this page 0%

I have been the person who copied a production database into a laptop “just for the bug.” The dump had emails, addresses, and a column named ssn. The bug was a null check. The dump stayed in ~/Downloads for a month. That is not a hypothetical scare story from a conference. That is me, earlier in my career, before I started treating fixtures as a product.

Randomly exists because I want synthetic values that pass format checks without being anyone. Names, emails, addresses, SSN-shaped strings, cards, IBANs. This post is the policy I wish I had printed above every staging cluster: no prod dumps, no “anonymize with MD5,” no reuse of a coworker’s real identity because it “looks more realistic.”

Production database icon crossed out next to a box of labeled synthetic fixture files
The bug is in the code. The dump is a second incident waiting for a USB stick.

Why prod dumps show up

They are convenient. The data has the weird last names, the plus-address emails, the suite numbers, the edge-case ZIPs. Staging looks “real.” Demos impress. Then a contractor gets VPN access. Then a screenshot of an admin page hits a ticket with a real child’s name. Then a log aggregator indexes the dump because someone ran a query with SELECT *.

GDPR, CCPA, and contract clauses are the formal version of this. The informal version is: you cannot un-send a Slack attachment. I assume every staging system is leakier than production. Fewer alerts. More debug prints. More “temporary” public buckets. If the data would embarrass you on the open web, it should not be in staging.

Subset dumps with “we hashed the emails” still join. MD5 of an email is a lookup key. I argued that in the MD5 versus SHA-256 post. If you need a stable fake email per original row, generate a synthetic email from a random ID, store the mapping in a locked table if you even need a mapping, and do not keep the original. Most bugs do not need a mapping.

What I generate instead

People: the American names tool for US-looking fixtures. Mix first and last independently. Watch uniqueness if your test asserts on full name. Real populations have collisions. Your 20-row suite can too.

Emails: random emails on domains you own or reserved ones like example.com. Do not emit Gmail addresses. Someone owns those. Bounce tests will bother humans. Plus-tags are useful (qa+order42@example.com) when you control the inbox.

Addresses: random USPS-shaped lines, not a guarantee of a deliverable point. Do not use a real celebrity home. Do not use your own home. I have seen both in seed files.

SSNs: format-only. Area, group, serial shape, with invalid ranges where the SSA never issued, or clearly synthetic. The SSN generator is for that shape, format-only, so nobody thinks we mint tax IDs. Never use a real SSN in a test. Not even “the SSA example.” Especially not a coworker’s.

Grid of synthetic name, email, address, and SSN-shaped fields labeled fixture
Four fields that satisfy forms. None of them should survive a grep of production.

Payment and bank shapes

Cards: Luhn-valid, never charged. See Luhn test cards. Processor test PANs when you are in their sandbox. Generated PANs when you need volume and uniqueness.

ABA routing: 3-7-1 valid, not a live origination. IBAN: mod-97 valid. Same warning as always: checksum is not an account.

Put all of these in a directory named like testdata/synthetic/. Do not name it customers. Do not use production column comments that say “PII” without also saying “FAKE.” Future you will run a scanner and panic, or worse, the scanner will ignore the file because of the path. Pick a path convention and teach the scanner.

Staging leaks I have seen

Error trackers. Sentry will happily keep request bodies. Disable PII in the SDK. Then still do not put SSNs in query strings.

Session replay. It is a video of the user’s screen. Synthetic data still looks like PII to anyone watching. Mask those fields anyway. You will switch the environment URL one day and forget.

CI artifacts. Failed tests upload HTML snapshots. The snapshots include the fixture PAN. Public CI on a public repo is a publishing platform. Use private artifacts or redact.

Marketing sites and CMS. I work on a public site. I do not paste fixture SSNs into blog screenshots. I use obviously fake strings and cropped UI. If you need a picture of the SSN generator, generate a fresh one for the image and accept that it will be indexed. That is still better than a real number.

Support exports. CSV from staging mailed to a vendor. Treat staging exports like production exports if the data was ever real. If it was synthetic, still ask why a vendor needs it.

Leaky staging pipeline: dump file, chat upload, error tracker, and a public CI artifact
Staging is a hallway with more doors than production. Fill it with fakes.

Rules I actually follow

No production snapshots on laptops. If I must reproduce a bug that depends on a real row, I reproduce the shape of the row, not the identity. I will copy the string length and the validation error, not the email.

No shared “celebrity” fixtures that are actually real people. There is a Wikipedia-famous SSN example that still circulates. Do not.

Rotate anything that appeared in a conference talk. Screenshots get OCR. Talk videos get transcripts. I rotate demo passwords too. Generate a new one locally. Do not reuse the talk password.

Separate seeds per environment. The email that receives staging mail should not be a real customer. Use a catch-all on a domain you own.

Document the generators in README. If a new hire cannot find how to mint a user, they will import prod. That is a docs failure, not a moral failure of the hire.

What synthetic cannot do

It will not find the bug that only happens for a specific real last name encoding, unless you include that encoding on purpose. Add a row with a hyphenated name. Add a row with a non-ASCII character. Add a row with an empty apartment field. You can do that without cloning ten million real people.

It will not match production distributions. If you need skew (lots of one state, few of another), script the skew. Do not import it from customers.

It will not satisfy a regulator who asked for a production-like penetration test with real data. That is a scoped, legal, monitored activity, not a Friday dump. I am not that engagement. I am the blog post that tries to make the Friday dump unnecessary for ordinary QA.

UUIDs for row IDs should be v4 in fixtures so they do not embed the afternoon you generated the file. I generate them locally and I keep the clock out of the ID.

Phone numbers belong in the same bucket. A US-shaped number is not a reason to use your cell. Generate a token-like string if the field is really an OTP secret, or a plausible NANP number in a reserved range if the field is a phone. Do not SMS a real person from staging. Carriers remember. So do the people who receive “Your code is 000000” at dinner.

I also freeze locale. If the app is US-only, generate US addresses and US names. If you later add a DE address, you will find the ZIP regex. That is a feature. A dump from production would have given you DE for free and also given you a real Berliner. I would rather discover the regex with a synthetic Berlin row I typed on purpose.

Access control on staging is part of PII hygiene. Synthetic data still includes passwords and session cookies. Lock staging. SSO. No shared admin/admin. Generate the staging admin password. Put it in a team vault. The generator is local. The vault is where it lives after you copy.

When a designer asks for “real looking” copy, I give them synthetic looking copy that still has the right length. A 40-character last name. An email that wraps. An address line 2. Realistic layout does not require realistic humans. If the design QA still wants a famous name, use a fictional character you have rights to joke about, not a customer.

Lawyers will ask whether synthetic SSNs could collide with living people. Format-only generators that stick to invalid area numbers reduce that. I still treat any 9-digit tax-shaped string as sensitive in logs because DLP will, and because a collision, however unlikely, is a support nightmare. Do not search the web for a generated SSN to “see if it is taken.” That is how you write the number into a search index.

Frequently asked questions

Is example.com safe for emails?

It is reserved. Delivery will not hit a random human. You still need an inbox if the test waits on a message. Own a domain for that.

Can I scramble production in place?

People try. They leave a join key. They forget a table. Generating from scratch is boring and more honest. If you scramble, have a written list of columns and a test that greps for known production emails.

What about photos and documents?

Do not copy customer scans. Use generated PDFs with lorem text, or your own face if you like being in the fixture. Not a customer’s license.

Does Randomly keep what I generate?

The tools run in the browser. Read privacy. Your staging database is still your staging database. We cannot save you from a dump you load there.