The test data management decision record: why the production copy policy you chose determines your PII exposure surface and your test fixture coupling failure mode
Test data decisions are made in three founding sessions that never document the operational consequences — the test environment data session that copies the production database to staging on a weekly schedule without specifying which data categories require masking before leaving the production network perimeter (a 31-person B2B SaaS maintains a weekly pg_dump/restore job that copies 43,200 production user records including full names, email addresses, and phone numbers to staging; a contractor disables authentication middleware on a staging endpoint while integrating a third-party payment provider; the endpoint remains publicly accessible without authentication for 11 days before a security researcher discovers it through a misconfigured DNS record and downloads 12,400 user profiles; GDPR Article 33 requires notifying the relevant supervisory authorities within 72 hours and all 43,200 affected users within 30 days; the company incurs €180,000 in GDPR fines and €95,000 in legal, forensic, and user notification costs; the founding session documented "weekly production copy to staging for realistic test data" but never specified a data classification policy that identified which column types require masking before leaving the production boundary, a masking function per PII category, or an access control equivalence requirement that staging must meet before receiving a production copy); the shared fixture session that creates a shared mutable test database seeded by a fixture file at the start of each CI run without specifying fixture ownership or teardown requirements (6 months after the fixture is introduced, 23 fixture mutations have accumulated across 14 test files, each modifying shared state without restoring it; the test suite passes in 40% of CI runs and fails with confusing error messages in the remaining 60%, depending on which ordering the test runner chooses; the engineering team spends 3.5 hours per week on average diagnosing non-deterministic failures that reproduce only in specific orderings; the founding session documented "shared fixture database for integration tests — realistic data makes tests more meaningful" but never specified that tests modifying shared state must restore it in teardown, that each test should own the fixtures it depends on, or that test order randomization is the detection method for ordering-dependent coupling); and the synthetic data session that generates a handful of uniform synthetic records for the test environment without specifying the distribution properties the data must model relative to production (the team generates 500 synthetic user records with uniform activity distribution to avoid PII in the test environment; a reporting module with a 12-table join executes in 23 milliseconds against the 500-record test database; the same query executes in 5.8 seconds against production's 2.3 million records, whose user activity distribution is heavily skewed — 4% of accounts generate 71% of all product events; the query planner's execution plan choice differs between the uniform 500-record test data and the skewed 2.3-million-record production data because the planner's row count estimates and join order decisions depend on data statistics; the performance regression is not detected until 14 days after the feature ships, when enterprise accounts — the company's highest-value customer segment — begin filing support tickets about slow report loads; the fix takes 8 days to develop, test, and deploy; the founding session documented "use synthetic test data to avoid production PII in the test environment" but never specified that synthetic data must model the production distribution in record count, value skew, and column cardinality for the query planner to produce the same execution plan as production). What none of these sessions produce is the data classification schema that identifies which columns require masking before leaving the production network, the fixture ownership rule that requires each test to own the state it modifies, or the distribution specification that defines what production-representative means for synthetic test data.
A 31-person B2B SaaS company built their staging environment at the same time as their production environment, during year one when they had 8 engineers and their production database had 2,400 user records. The staging setup was a copy of production: the same Caddy configuration, the same application code, and a weekly cron job that ran pg_dump on the production database, transferred the output over SSH, and ran pg_restore on the staging database. The cron job had existed since year one and had been extended as the company grew. By year three, the production database held 43,200 user records including full names, email addresses, phone numbers, and hashed passwords. All 43,200 records appeared in the staging database every week after the Sunday night refresh. The staging environment was used by engineers for integration testing, by the QA contractor for manual test sessions, and by three third-party integration partners who had staging API credentials. The access control model for staging was lighter than production: staging API keys were stored in a shared 1Password vault accessible to all engineers and contractors, the staging environment had no network-level isolation from the company's office and contractor VPN ranges, and no security scanning was applied to staging deployments.
In March, a contractor was hired to integrate a new payment provider's webhook verification library into the application. The payment provider's staging API required that the webhook endpoint be publicly accessible (their verification system sent test events from IP addresses outside the company's network). The contractor disabled the application's OAuth middleware on the /webhooks/payment endpoint to allow the payment provider's test events through without authentication — the standard approach for webhook integration testing. They also, incorrectly, disabled the middleware on the parent router rather than the specific endpoint, which removed authentication from all /webhooks/ and /api/ paths in the staging application. The misconfiguration was committed on a Tuesday and was not caught in code review because the OAuth middleware configuration was in an infrastructure file that the senior engineers did not routinely review. The staging deployment ran the misconfigured code.
For 11 days, the staging environment's entire API was publicly accessible without authentication. A security researcher who ran periodic scans of subdomains associated with known B2B SaaS companies discovered the staging subdomain via a DNS record that had been inadvertently added to a public zone file during a Cloudflare migration six months earlier. The researcher called the /api/users endpoint, received a JSON paginated response of user records, and downloaded 12,400 records across 248 pages before noticing that the data included real names, email addresses, and phone numbers and stopping. The researcher submitted a responsible disclosure report. The company discovered the misconfiguration on day 11. The GDPR clock started from the moment of discovery: 72 hours to notify the relevant supervisory authority (the Dutch DPA, as the company's legal entity was registered in the Netherlands) and 30 days to notify all 43,200 affected individuals. The incident response cost included external legal counsel specializing in GDPR (€45,000), forensic analysis to determine the full scope of data accessed (€28,000), user notification infrastructure and communications (€22,000), and the DPA engagement process. The Dutch DPA imposed a €180,000 administrative fine for inadequate technical measures under Article 32 — specifically, for maintaining real PII in a non-production environment without access controls equivalent to the production environment. Total cost: €275,000. The founding session that established the staging environment documented "weekly production copy for realistic testing" and no more. It did not specify that PII must be masked before leaving the production network boundary, which columns required masking, what the masking function should be for each PII category, or that the access control model in staging must be equivalent to production if staging holds real PII. The implicit assumption was that the staging environment, while less secure than production, was an internal system that wouldn't be exposed to the internet. That assumption was correct for year one and incorrect by year three.
A 24-person SaaS company introduced integration tests in their second year after a pair of production incidents caused by untested interactions between the subscription billing module and the payment method management module. The team set up a test database initialized by a fixture file that seeded 50 users, 20 organizations, and 200 subscription records — enough data to exercise the billing logic without having to create records in every test. The fixture was seeded at the start of each CI run and the same test database was used by all tests in the run. The approach worked well initially: the fixture data was realistic, tests ran quickly against a pre-populated database, and the integration test coverage grew from 12 tests in month one to 118 tests by month six.
By month six, the test suite had accumulated a pattern that no individual test author had planned but that had emerged from six months of incremental additions. Tests that needed to verify a payment-related behavior would look up a user from the shared fixture and modify their payment method as part of the test setup — changing it to a test card number, or marking it as expired, or removing it entirely — then verify the behavior, then proceed to teardown without restoring the payment method to its pre-test value. The pattern originated in month two when a developer wrote a test that created its own user, modified that user's payment method, verified the behavior, and then deleted the user in teardown. The test was correct and self-contained. In month three, a different developer copied the test's structure but looked up an existing fixture user rather than creating a new one — the fixture user had 6 months of subscription history that made certain billing edge cases easier to test. The developer modified the fixture user's payment method in setup and did not restore it in teardown, because the original test they copied cleaned up by deleting the user, which was not appropriate for a shared fixture user. The payment method was left in the post-test state.
By month six, 23 distinct test methods had modified the payment method field of shared fixture users without restoring it. Each of these 23 tests assumed that the payment method was in a specific state at the start of the test — either the original fixture value, or the value set by a preceding test in a specific ordering that had become load-bearing. The test suite passed reliably in the ordering that the CI runner chose by default (alphabetical by file name, then by test method name within the file), because the 23 ordering dependencies happened to be satisfied by that ordering. When the team upgraded their test runner to a version that randomized test execution order for better isolation, the suite began failing in 60% of CI runs. The failures were confusing: they reported assertion errors in the billing module (a payment method field had an unexpected value) and the payment alert module (a payment method field was missing that the test expected to find), with no obvious connection to the tests that had modified those fields. The team spent 3.5 hours per week on average over the following three months diagnosing non-deterministic failures — running the suite multiple times, comparing failure patterns across runs, and ultimately building a custom test-run comparison tool that diffed two runs with different orderings to identify which tests produced different results in each ordering. They found 23 test methods that behaved differently depending on ordering and traced each back to a shared fixture mutation without teardown. Fixing all 23 required either converting each test to own its user (create the user, modify it, verify, delete in teardown) or adding explicit payment method restoration to teardown for tests that could not create their own user due to the complexity of the subscription history required. The total repair effort was 18 engineering days spread across three sprints. The founding session that introduced the shared fixture database documented "shared fixture database — realistic data for integration tests" and no more. It did not specify that tests must not modify shared state without restoring it, that shared fixtures should be read-only for state that other tests depend on reading, or that test order randomization should be part of the CI configuration from the start to detect ordering dependencies when they are introduced rather than six months later.
A 38-person analytics SaaS company built a reporting module in their third year that allowed customers to generate aggregated reports across their entire product event history. The module's core query joined 12 tables — the users table, the organizations table, the subscriptions table, the product event log partitioned by month, the event category taxonomy, and seven supporting dimension tables — and applied three levels of aggregation (per-event, per-day, per-organization) before returning the final report data. The query was written by a senior engineer with extensive SQL experience and was reviewed by two other engineers before merging. The test environment had 500 synthetic user records distributed uniformly across 12 industry categories, with each user generating an average of 50 product events per month, for a total of approximately 25,000 events across the 500-record test database. The test environment was used for both correctness testing (does the query return the right aggregated values?) and a rough performance check (does the query run in under 1 second?). Against the 500-record test database, the query ran in 23 milliseconds — well under the 1-second threshold. The feature shipped.
Production had 2.3 million user records accumulated over three years. The distribution was far from uniform: 4% of users were enterprise accounts (92,000 users) that generated 71% of all product events — an average of 840 events per user per month, compared to 12 events per month for the remaining 96% of users. The enterprise accounts were also concentrated in 3 of the 12 industry categories (financial services, healthcare, and logistics), meaning that those three category table partitions had 74% of the total event volume and 9 of the 12 dimension table join paths were exercised almost exclusively by enterprise account queries. The PostgreSQL query planner, when analyzing the test database's uniform 500-record distribution, chose a hash join strategy that was optimal for uniform data: it estimated similar row counts across all 12 industry categories and built hash tables sized for those estimates. On production's 2.3 million skewed records, the planner's row count estimates for the financial services, healthcare, and logistics partitions were 24 times larger than its estimates for the other 9 categories, and the optimal join order changed: a nested loop with an index scan on the event_category_id column outperformed the hash join for the high-skew enterprise categories, while the hash join remained optimal for the low-skew categories. Because the planner had been calibrated on the uniform test data statistics, it chose the hash join for all categories in production — a plan that was optimal for the minority of queries and suboptimal for the majority. Enterprise account queries, which generated 71% of the query volume, ran in 5.8 seconds. Standard account queries ran in 180 milliseconds. The pre-existing response time standard was 800 milliseconds.
The performance regression was not detected immediately after the feature shipped because production traffic ramped gradually — the feature was announced in a changelog and enterprise accounts began generating reports over the following days rather than all at once. The first slow-report support tickets arrived 6 days after the feature shipped. The support team initially attributed them to account-specific data volume and closed them with "we're investigating." By day 14, the support queue had 31 open slow-report tickets from enterprise accounts and the pattern was clear. The engineering team ran EXPLAIN ANALYZE on a representative enterprise account query against a production replica and identified the hash join plan choice as the root cause. The fix required two changes: a composite index on the event log's (org_id, event_category_id, event_date) combination that the nested loop could use efficiently, and a query rewrite that split the enterprise and standard account paths at the application layer, using different join strategies for each. The fix took 8 days to develop (3 days), test against production-replica data (2 days), and deploy with a controlled rollout (3 days). Enterprise accounts experienced 5.8-second report loads for 22 days. The founding session that decided to use synthetic test data documented "500 synthetic users for integration testing — avoids PII, fast CI runs" and no more. It did not specify that synthetic data must model the production distribution in total record count, user activity skew, and event volume per industry category for the query planner to produce the same execution plan as production, that a performance validation step must run against production-representative data volumes before any query-heavy feature is deployed, or that the EXPLAIN ANALYZE output on the test environment is not a reliable predictor of the production execution plan when the test data distribution differs from production.
Structural properties set by the test data management decision
Three structural properties are determined when a team establishes their test data management policy. None appear explicitly in the session that sets up a staging environment, the session that introduces integration test fixtures, or the session that decides to use synthetic data to avoid PII — they are the operational consequences of design choices made under the assumption that "test data" means "data that is good enough for the test to pass" without specifying what "good enough" requires across the dimensions of privacy compliance, test isolation, and production representativeness.
Property 1: The production copy policy and the PII exposure surface. The PII exposure surface of a non-production environment is the set of conditions under which real user data held in that environment becomes accessible to an unintended audience. For a staging environment that contains a full production copy, the surface includes: authentication or authorization misconfigurations on any staging endpoint (the most common incident class — staging has more frequent configuration experiments than production, more third-party integrations in progress, and lighter change review); DNS records that expose the staging subdomain to internet-accessible address space (staging subdomains are routinely added to public DNS zones for webhook integration testing and never removed); access by engineers, contractors, and third-party partners who have staging credentials and whose security posture the company cannot fully control; and logging or debugging tools that capture request or response data and transmit it to external SaaS platforms (Sentry, Datadog, LogRocket) whose data retention policies are outside the company's control. The exposure surface shrinks to near-zero when the production copy policy is: data is masked before leaving the production network boundary. The masking runs as a mandatory transformation in the data pipeline between the production dump and the staging restore — the output of the production dump never touches staging-accessible storage without masking applied. The masking policy is defined in a data classification schema that lists every table and column containing PII categories under GDPR Article 4 definitions, and the deterministic masking function for each category. The schema is version-controlled alongside the application schema, so that when a database migration adds a new column containing PII, a schema review step catches the omission of the new column from the masking policy before the next staging refresh includes the unmasked column. The data governance decision record documents the data classification taxonomy — the categories of personal data the company holds, the legal basis for processing each category, the retention limits, and the column-level classification for each table — which provides the authoritative source for the test data masking policy. The compliance automation decision record documents the GDPR compliance automation approach, including the automated scanning that verifies PII columns are classified in the governance schema and masked in the staging pipeline before the staging refresh runs. The alternative to the masked-copy approach is synthetic data generation — the staging environment contains no real data, only data generated by a synthetic data generator that models the production schema and distribution. Synthetic generation eliminates the masking pipeline entirely but requires an investment in a generator that is updated when the schema changes and validated to produce data that is realistic enough for the integration tests that depend on realistic data shapes (referential integrity, value format, category distribution). Both approaches are correct; the masking pipeline is lower upfront investment and higher ongoing maintenance; synthetic generation is higher upfront investment and more robust once established. The security scanning decision record documents the security controls applied to staging deployments — authentication coverage verification, network access scope confirmation, and the requirement that any staging deployment that exposes a new endpoint to the network undergoes an authentication coverage check before the deployment goes live, which catches the class of misconfiguration that exposed 43,200 user records.
Property 2: The fixture ownership model and the test coupling surface. The test coupling surface is the set of paths through which one test's execution affects another test's result. In a test suite with shared mutable fixtures, the coupling surface includes every fixture field that any test modifies without restoring — each such field is a potential ordering dependency whose violation produces a test failure that is visible only in the orderings that expose the dependency. The coupling surface grows monotonically as tests are added: each new test that modifies a shared fixture field without teardown adds one more ordering dependency to the surface. The growth is invisible in normal CI runs because the default test runner ordering satisfies all existing ordering dependencies by chance, and a new test that adds a new dependency passes in its first run. The growth is only visible when ordering dependencies are explicitly probed — by randomizing test execution order across multiple CI runs and comparing which tests produce different results across orderings. The fixture ownership model that prevents surface growth is: each test owns the state it modifies. Ownership means setup creates the state and teardown destroys or restores it, with no state surviving from one test's scope into another test's scope. The two ownership patterns are isolation (each test creates its own records in setup and deletes them in teardown — the strongest form, compatible with parallel test execution because each test's records are in a separate database scope identified by a test-run-specific namespace) and scoped sharing (read-only shared fixtures for records that tests must read but never modify, such as product catalog entries, permission definitions, and system configuration records; per-test-class mutable fixtures for records that tests must modify, owned by the test class's setup and teardown methods). The boundary between the two patterns is whether the test modifies the fixture: if it reads without modifying, the fixture can be shared; if it modifies, the fixture must be owned by the test class or by the individual test. The detection mechanism for ownership violations is test order randomization with consistent seeding: running the test suite N times with N different random orderings and comparing which tests produce different outcomes across orderings identifies the tests that have ordering dependencies, which are the tests with unowned shared state modifications. The CI/CD pipeline decision record documents the pipeline configuration for test isolation validation — the CI step that runs the test suite with randomized ordering on every PR branch and fails the build if any test produces different results across orderings. Running randomized-order tests in CI catches new ordering dependencies at the moment they are introduced rather than 6 months later. The test strategy decision record documents the test type taxonomy and the fixture scoping rules for each type — which test types may use shared fixtures (read-only reference data), which must use class-scoped fixtures (integration tests for a single module), and which must use test-scoped fixtures (end-to-end tests that exercise cross-module state). The framework-specific implementation of each scoping level (Jest's beforeEach/afterEach, pytest's fixtures with scope=function/class/session, JUnit's @BeforeEach/@AfterEach) is specified in the test strategy decision record rather than left to each developer's convention. The database migration strategy decision record documents the test database schema migration process — how the test database schema is kept synchronized with the production schema, and whether the fixture seed is updated when migrations add or remove columns, to prevent the coupling failure mode where a migration changes a column that a shared fixture populates with a hardcoded value that is now invalid under the new schema.
Property 3: The synthetic data distribution model and the production-representative surface. Synthetic test data that does not model the production distribution produces three distinct classes of false-pass failures — query plan divergence, N+1 visibility threshold, and memory boundary crossing — each caused by the test data operating at a different statistical point than production. Query plan divergence occurs because the PostgreSQL (and MySQL, SQL Server) query planner makes join order and join strategy decisions based on table statistics (row counts, column cardinality, most-frequent values, histogram buckets) stored in the database's catalog after ANALYZE runs. When the test database's statistics reflect a 500-record uniform distribution and the production database's statistics reflect a 2.3-million-record skewed distribution, the planner produces different execution plans — a hash join on the test side, a nested loop with index scan on the production side — and the performance characteristic of each plan at the respective data volume determines the observed query time. A query that takes 23ms on test with a hash join may take 5.8 seconds on production with the same hash join strategy, while the nested loop plan would take 800ms on production. The test validates correctness but not plan choice; only a production-representative data volume and distribution validates plan choice. N+1 visibility threshold occurs because an N+1 query bug generates 1 additional query per record processed: at 500 records, this is 500 additional queries (invisible in a 23ms baseline); at 2.3 million records, this is 2.3 million additional queries (catastrophic). The N+1 is present in the test environment but the additional 500 queries complete fast enough that no performance threshold is breached, masking the problem. Memory boundary crossing occurs because batch processing code that loads all matching records into memory before processing operates correctly when the record count fits in JVM or Node.js heap; at production scale, the same code exhausts heap and OOMs or triggers excessive GC pauses. Each of these three false-pass classes is prevented by a different validation step: query plan divergence by running EXPLAIN ANALYZE against a production-representative database (either a production replica with masked PII, or a synthetic database whose statistics match production); N+1 detection by query count assertion (each integration test asserts a maximum number of database queries for a given input size, with a separate assertion that the query count grows sub-linearly as input size grows); memory boundary by load testing at production-representative record counts before shipping any feature that processes all records in a collection. The distribution specification for synthetic data must include: the total row count for each table at a specified multiple of the current production count, the skew ratio for tables with heavy-tail distributions (top decile generates X% of events), and the cardinality for each join column (distinct values per table per column). These parameters are measured from production using aggregate statistics views (pg_stats, information_schema) and stored in a data specification file alongside the synthetic data generator. When the production distribution changes significantly (a major customer class grows from 4% to 12% of users), the specification is updated and the generator re-run so that test data stays representative. The performance optimization decision record documents the performance validation process — the pre-deploy performance gate that requires EXPLAIN ANALYZE output review for any query touching tables above a size threshold, the production-representative load test that must pass before any query-heavy feature is deployed, and the performance regression alert thresholds that fire when a production query's p95 execution time increases more than 20% from its pre-deploy baseline. The database indexing strategy decision record documents the index creation policy for new queries — the process by which EXPLAIN ANALYZE output is used to identify missing indexes before the feature deploys, rather than after a performance regression is discovered in production.
What the founding session records and what it omits
The founding test data session typically records the test environment type (staging, CI, developer laptop), the data source (production copy, synthetic, anonymized subset), and the rationale for that choice (production copy = realistic data, synthetic = PII-free, anonymized = balance). It may record the tooling (pg_dump, factory_bot, faker, a custom seed file) and the refresh cadence (weekly, daily, per-CI-run, on-demand). What it does not record is the data classification policy: which columns contain PII under GDPR Article 4 definitions, what the masking function is for each category, and at what point in the pipeline the masking is applied relative to the data leaving the production network. Without the classification policy, the masking boundary is undefined — masking might happen in the staging restore script, or in the application's test setup, or not at all, and each location has different security guarantees. The masking that happens inside the production network (the pg_dump pipe is piped through a masker before the output is written to an S3 bucket accessible from staging) provides a guarantee that no real PII exists in staging-accessible storage. The masking that happens in the staging restore script (the dump file is transferred to staging and then masked) provides no guarantee for the window between the dump file arriving in staging storage and the masking completing — a window that is small in the typical case and arbitrarily long in the case of a restore failure. And no masking at all provides no guarantee, which is the state most staging environments are in when the PII exposure incident occurs.
The founding session also does not record the fixture ownership model for integration tests, because the team writing the first integration tests is typically writing a small number of tests where the shared fixtures are simple and the coupling surface is small. The coupling surface is zero when there is one test that modifies one fixture field and one test that reads that field — the dependency exists but has no impact as long as the reader always runs after the writer. The coupling surface becomes visible when the number of test-fixture pairs grows, because the probability that a random ordering satisfies all dependencies simultaneously decreases as the number of dependencies grows. The correct time to specify the fixture ownership model is before the first integration test is written — establishing whether the suite will use isolation (each test creates and destroys its own state) or scoped sharing (read-only shared, class-owned mutable), what the teardown requirement is for each pattern, and how the CI pipeline will detect violations of the model. Establishing the model before the first test is written is a 30-minute design decision that prevents 18 engineering days of retroactive repair. Establishing it after 23 ordering dependencies have accumulated is a 3-sprint refactoring project. The founding session didn't make either of those decisions — it made the de facto decision that shared mutable fixtures are fine by introducing them without a model, and the surface grew from that implicit choice.
The founding session also does not record the distribution requirements for synthetic test data, because the team generating synthetic data is focused on a different problem — avoiding PII — and the requirement that synthetic data model production distribution is not obviously related to that problem. The connection is indirect: synthetic data avoids PII, but to avoid PII while also being production-representative for performance testing, it must model the production distribution in the dimensions that affect query planning and algorithm complexity. A team that generates synthetic data with uniform distribution to avoid PII achieves one goal (no PII in the test environment) and fails the other (the test environment's performance characteristics don't represent production's). The distribution specification is the bridge between the two goals: it defines what production-representative means in terms that a synthetic generator can implement without copying real values. Cardinality, row counts, and frequency distributions are aggregate statistical properties that contain no PII and can be exported from production. Including the distribution specification in the test data decision record — alongside the PII avoidance rationale — makes both goals explicit and prevents the false confidence that a passing test environment performance check provides when the test data has a fundamentally different statistical shape than production.
The accumulated cost of omitting these three specifications is substantial but arrives at different timescales. The PII exposure omission produces a low-probability high-severity incident — years may pass before a staging misconfiguration exposes real data, but when it does, the cost is measured in regulatory fines, user trust erosion, and legal spend. The fixture ownership omission produces a medium-probability medium-severity degradation — ordering-dependent failures appear within months as the test suite grows, and the cost is measured in engineering hours spent on diagnosis and repair and in reduced confidence in the test suite as a reliability signal. The distribution omission produces a high-probability medium-to-high-severity incident specific to query-heavy or data-intensive features — every such feature that is tested against non-representative data has some probability of a production performance regression, and the cost is measured in degraded performance for the company's most valuable customer segment and the engineering time required to diagnose and fix a regression that a production-representative test would have caught before deployment. The data retention decision record documents the data lifecycle policy that determines how long test environment data is retained, when test databases are wiped and re-seeded, and which test data falls under the same retention requirements as production data (typically: any test data that was derived from production records must be retained for the same period as the production source, because GDPR's right to erasure applies to derived data — if a user requests deletion, their masked record in the staging database must also be deleted). The audit log decision record documents the audit trail for test data access — the access log that records which engineers, contractors, and third-party systems have accessed the staging environment and what data they read, which is the forensic input when a test environment incident requires determining the scope of what was accessed and by whom. The WhyChose decision extractor finds the founding test data sessions in your ChatGPT and Claude export — the "what data should we use in staging?" planning session, the "how should we structure integration test fixtures?" discussion, the "how do we avoid PII in test environments?" investigation. It extracts the staging data policy, the fixture structure choice, and the synthetic data approach from the founding sessions and surfaces the masking boundary, fixture ownership model, and distribution specification that the sessions documented versus the ones they omitted — the decisions that determine whether a staging misconfiguration exposes 43,200 real user profiles or a set of masked records with no PII value to an attacker.
The five ADR sections for a test data management decision
Section 1: Data classification policy and PII masking requirements per environment tier. Specify the environment tiers in scope for the policy (production, staging, CI, developer laptop) and the data source allowed for each tier. For each non-production tier, specify the data source: production copy with masking applied (specify at which stage in the pipeline the masking runs and by which tool), synthetic data generated by a generator (specify the generator, the schema it models, and the distribution specification it targets), or an anonymized subset (specify the anonymization method and the minimum anonymization strength required). For tiers that use a production copy, specify the data classification schema: the list of tables and columns containing PII under GDPR Article 4 categories, the masking function for each category (deterministic hash, format-preserving masking, constant substitution, null replacement — and the rationale for each choice, since deterministic hashing preserves referential integrity across foreign keys while constant substitution does not), and the masking pipeline stage at which each column is transformed. Specify the masking boundary: the network or storage boundary that masked data must not cross with real values. Specify the classification schema update process: when a database migration adds a new column, the migration PR must include a classification entry for the new column (PII or non-PII, and if PII, the category and masking function). Specify the masking validation test: a CI step that runs after every staging restore and verifies that no row in any classified column contains a value pattern that matches real PII (for email columns: the domain is .test or the local part is a hash string, not a real name; for phone columns: the format is in the reserved test range). The data governance decision record is the authoritative source for the data classification taxonomy referenced by this section. The compliance automation decision record documents the automated compliance check that verifies the masking pipeline ran before the staging database was last refreshed.
Section 2: Test environment access control equivalence model. Specify the access control requirements for each environment tier that holds data derived from production (including masked copies). For tiers whose data has been masked with deterministic functions: specify the access control level required — a masked database whose masking function is reversible by anyone with the masking key requires production-equivalent access control; a masked database whose masking function is irreversible (one-way hash) requires access controls proportional to the residual data sensitivity (masked email addresses may still identify users if the hash is short enough to brute-force). Specify the authentication requirement for staging API endpoints: every staging endpoint that returns user data must require authentication by default, with an explicit exception process for endpoints that must be publicly accessible for third-party integration testing. The exception process requires a security review, a time-bounded exception (maximum 72 hours for webhook integration testing), and an authenticated wrapper that can be enabled without deploying a code change (an Nginx allow-list by partner IP range, or a separate endpoint path that does not strip authentication). Specify the network isolation requirement for staging: staging must be on a network segment that does not route to the public internet except through explicit allowlist entries, and the allowlist must be reviewed when new third-party integrations are added. Specify the staging credential management policy: staging API keys must not be stored in shared credential stores that are accessible to all engineers and contractors — they must be scoped by team and purpose, and contractors must have staging credentials that expire when the engagement ends. The security scanning decision record documents the security scanning policy for staging deployments — authentication coverage verification (automated scan that calls every API endpoint from outside the staging network and verifies that all non-whitelisted paths return 401 rather than data) runs on every staging deployment, not just production deployments.
Section 3: Test fixture ownership and mutability policy. Specify the fixture ownership model for each test type in the test strategy. For unit tests: all fixtures are test-scoped — each test creates its dependencies in setup and removes them in teardown; no shared state crosses test boundaries. For integration tests: fixtures are classified into two categories: read-only reference data (product catalog entries, permission definitions, system configuration records that tests never modify — these may be seeded at the test suite level and reused across all tests without teardown because they are never mutated); and mutable fixtures (user records, organization records, subscription records, any record whose state tests must modify to verify behavior — these are class-scoped, owned by the test class's setup and teardown methods, and may not be shared across test classes). Specify the teardown requirement for mutable fixtures: any test method that modifies a class-owned fixture must restore the modified fields to their pre-test values in a try/finally block that guarantees teardown even when the test assertion fails. Specify the ordering-dependency detection mechanism: the CI pipeline runs the integration test suite with randomized test execution order on every PR branch, using a different random seed per run across three runs, and fails the build if any test produces a different outcome across orderings (a different assertion failure, a different exception, or a pass vs. fail difference). Specify the linting rule for shared state access: a static analysis check (or a test framework plugin) verifies that no test method writes to a field in a shared fixture object without including a teardown restoration for that field. Violations fail the lint check and block the PR. The CI/CD pipeline decision record documents the integration of the ordering-dependency detection step and the fixture ownership lint step into the CI pipeline. The test strategy decision record documents the test type taxonomy and the fixture scope rules for each type — which test types use which fixture ownership pattern and the rationale for each choice.
Section 4: Synthetic data distribution specification and production-representative testing requirement. Specify the distribution parameters that synthetic test data must model for each environment tier that uses synthetic data. The parameters are measured from production using aggregate statistics views (pg_stats in PostgreSQL, information_schema.table_statistics in MySQL) and stored in a data-specification file version-controlled alongside the application code. Required parameters per table: total row count (specify as a target fraction of the current production count — typically 10% minimum for integration tests, 100% for performance tests), per-column cardinality for all columns used in WHERE clauses or JOIN conditions, the frequency distribution for columns with heavy-tail distributions (the top-decile value frequency, the Gini coefficient if available, or at minimum the ratio of the 90th percentile row count to the median row count for user-activity-type tables), and the referential integrity graph (which foreign key relationships must be preserved in the synthetic data to produce valid join results). Specify the production-representative performance testing requirement: any feature that introduces a query touching tables above a defined row-count threshold must pass a performance gate that runs the query against a production-representative dataset (either a masked production replica or a synthetic database whose statistics match the distribution specification) and verifies that the EXPLAIN ANALYZE output shows the expected execution plan and that the execution time is within the defined performance budget. The performance gate is a pre-deploy requirement — the feature may not deploy without a passing performance gate result for all queries above the threshold. Specify the query count assertion requirement for N+1 detection: integration tests for query-heavy features assert a maximum number of database queries for a defined input size, and a separate assertion verifies that the query count does not scale linearly as the input size doubles (N+1 queries scale linearly; correct queries are sublinear). The performance optimization decision record documents the performance gate configuration — the row-count threshold, the performance budget per query type, the EXPLAIN ANALYZE review checklist, and the reviewer qualifications for passing a performance gate. The database indexing strategy decision record documents the index creation policy for new queries — how EXPLAIN ANALYZE output from the performance gate is used to identify and create missing indexes as part of the same PR that introduces the feature, before the feature is deployed without the necessary indexes.
Section 5: Test data lifecycle, cleanup policy, and compliance audit requirements. Specify the retention policy for non-production data derived from production. Data in staging derived from production (even masked) is covered by GDPR's data minimization principle (Article 5(1)(c)) — it must not be retained longer than necessary for the testing purpose. Specify the maximum retention period for staging data (typically the refresh cadence plus a one-cycle buffer — if staging refreshes weekly, the maximum retention is 14 days), after which the staging database must be wiped or overwritten by the next refresh. Specify the developer laptop policy: developers may not retain local copies of production-derived data (masked or otherwise) beyond the duration of the task that required the local copy. Specify the CI test database policy: CI test databases are ephemeral — they are created for the duration of the CI run and destroyed immediately afterward, with no persistent storage that retains data across runs. Specify the compliance audit requirement: the test data management policy must include a quarterly audit that verifies the staging refresh pipeline is masking all classified columns (by running the masking validation test against the live staging database and comparing classified column values against the expected masked-value patterns), that staging credentials are scoped and expire on schedule, and that no developer laptops or CI systems retain production-derived data beyond the retention policy. The audit results are recorded in the compliance log alongside the production compliance audit. Specify the right-to-erasure propagation requirement: when a user exercises their GDPR right to erasure and their records are deleted from production, the deletion must propagate to all environments that hold data derived from that user's records — staging (masked copy), CI databases if persistent, and any developer local copies known to hold data from the last refresh cycle. The propagation process must complete within the 30-day response deadline for right-to-erasure requests. The data retention decision record documents the production data retention policy, the right-to-erasure workflow, and the systems covered by each, which is the authoritative source for extending the retention and erasure requirements to non-production environments. The audit log decision record documents the access audit log for non-production environments — the log that records who accessed what data in staging and CI, providing the forensic input for determining the scope of a test data management incident and the evidence base for the quarterly compliance audit.