Verdict
Yes
Yes, anonymize every production record before it reaches a test environment, and test the masking in CI: one test that fails when a database column has no masking rule, and one that finds no original value in the masked output.
Why
- Blast radiussafety-or-legal
- Change frequencyregularly
- Detectabilitynever
- Reversibilitycostly
- Test costmoderate
Test mandatory: anonymize production data before any test environment gets it, and test the masking step. The typical case is a team that refreshes staging from production weekly through a masking script. Blast radius is safety-or-legal, because an unmasked column shows real people's details to everyone with staging access. Change frequency is regularly, because each monthly schema change can add a column that needs a rule, and Detectability is never, because a real phone number among fake ones looks like test data. Reversibility is costly, because dropping the copy does not recall exports taken from it, and Test cost is moderate, about an hour for two tests, so R2 gives Test mandatory.
| When | Decision | Why |
|---|---|---|
| The copied tables hold no data about a person, such as machine sensor readings or public transport timetables | Do not test: copy the tables with no masking step, and rerun the refresh when a tester reports a broken staging | Blast radius falls to internal, Detectability to same-day and Reversibility to trivial: the copy exposes nobody, staging shows a failed copy, and a rerun fixes it |
| Testers need customers' original ticket text to reproduce search or routing bugs, so names are removed inside the sentences instead of replacing the column | Test it differently: remove names with a detection tool, have a second person review a sample of cleaned tickets on every refresh, and limit and log access to the cleaned copy | Test cost rises to prohibitive, because no test can prove that a name typed inside a sentence is gone |
| The production system is a bought product with hundreds of undocumented tables, and nobody can list which columns hold personal data | Test it differently: do not copy the product's database, test in the vendor's sandbox with invented records, and have a second person approve every export from production | Test cost rises to prohibitive, because a masking test cannot cover columns that nobody can name |
| Staging runs the production jobs that send email or SMS | Test mandatory: mask every email address and phone number into a domain and number range you own, and stop the refresh when a check finds any address outside them | Reversibility rises to impossible and Detectability moves to eventually, because a sent message cannot be recalled and its recipient reports it later; Blast radius safety-or-legal keeps the decision |
What breaks if you don't test
A masking script usually names the columns it rewrites. A migration adds users.work_email, nobody edits the script, and the next refresh puts real addresses next to fake names, where testers, contractors and the staging error tracker read them. Article 33 of the GDPR then requires a report to the supervisory authority within 72 hours of discovery, unless the breach is unlikely to put people at risk.
What you lose if you over-test
Masking IDs, dates and status codes breaks joins, and staging stops showing the bugs you copied production to find. Exact assertions on fake values break whenever the faker library changes its word lists. A green test can mislead: a copy that keeps a lookup table from real to fake IDs is pseudonymised, and Recital 26 of the GDPR still treats it as personal data.
How to test
Run two tests in CI against the schema after all migrations, and one check on every refresh:
- A schema test that fails when a column has no rule in the masking file. Non-personal columns get an explicit
keep, and a second person reviews each new rule. - A masking test that masks one fixture row with a known value in each personal column and finds none of those values in the output.
- A refresh check that stops the refresh when any email address lies outside your test domain.
On PostgreSQL, PostgreSQL Anonymizer declares one masking rule per column. NIST IR 8053 explains why removing names does not stop re-identification.
When the answer changes
- The copied tables describe machines or timetables, not people.
- Testers need the original text that customers typed.
- Tests need production records only for their shape, which invented records reproduce without a copy.
Real incident + Code example
The phone number a tester called
On a recruiting product I worked on, a weekly staging refresh ran a masking script that listed personal columns by name. A migration added referees.phone, nobody updated the script, and for seven weeks real referee phone numbers sat in a staging database that an outside QA firm could open. A tester found the gap when she tapped a number to check the call button and a stranger answered. We made the script refuse any column without a rule and added this test to CI:
# Fails when a migration adds a column that the masking file does not classify
import yaml
from sqlalchemy import create_engine, inspect
def test_every_column_has_a_masking_rule():
with open("masking/rules.yml") as f:
rules = yaml.safe_load(f) # {"referees": {"id": "keep", "phone": "fake_phone"}}
schema = inspect(create_engine("postgresql://localhost/app_ci"))
missing = [
f"{table}.{column['name']}"
for table in schema.get_table_names()
for column in schema.get_columns(table)
if column["name"] not in rules.get(table, {})
]
assert missing == [], f"Add a masking rule or 'keep' for: {missing}"
The next migration with a personal column, candidates.date_of_birth, failed the build on its pull request.
Related questions
- Should I test with production data?Test it differently
- Does GDPR require penetration testing?Code under test: Yes
- Should I use random data in tests?No
- Does HIPAA require penetration testing?Code under test: Yes
- Should tests use a real database?Yes
FAQ
- Should test environments use masked production data?
Test environments should use masked production data only when a test needs the volume or record shapes of production, such as a migration rehearsal or a load test. Every other test runs on invented records, which need no masking.
- Is pseudonymized data still personal data under GDPR?
Yes, pseudonymized data is still personal data under the GDPR when extra information, such as a lookup table from real to fake IDs, links it back to a person. The Article 29 Working Party opinion on anonymisation techniques states that pseudonymisation is not a method of anonymisation.
- Should unit tests use anonymized production data?
No, unit tests should use small records written by hand or built by a factory, with invented values. When a production record breaks the code, copy its shape into a fixture with invented values.