Test data management
Give every test the data it needs, isolated from every other test, without copying sensitive production records into lower environments.
When to invoke
- "Our tests fail when run in parallel."
- "Set up factories instead of these fixture files."
- "How do we get realistic data into staging?"
- "Tests pass alone but fail in the suite."
- "Anonymize this production dump for testing."
Factories over shared fixtures
Shared fixture files create hidden coupling: one test's expectations depend on data another test also uses, so any change breaks unrelated tests.
# Factory: each test states only what it cares about
def make_order(**overrides):
return Order(**{
"id": uuid4(),
"status": "pending",
"total": Decimal("10.00"),
"created_at": FIXED_CLOCK,
**overrides,
})
def test_refund_rejected_when_already_refunded():
order = make_order(status="refunded") # intent is visible in the test
The test declares the one attribute that matters. Everything else is a sane default that can change without touching this test.
Isolation strategy
Pick one and apply it consistently.
| Strategy |
How it works |
Trade-off |
| Transaction rollback |
Wrap each test, roll back after |
Fastest; cannot test committed behavior or multi-connection flows |
| Reset and reseed |
Clear tables between tests, then reload a known baseline |
Simple and thorough; slower as the schema grows |
| Unique namespacing |
Every record uses a run-scoped identifier |
Parallel-friendly; requires discipline everywhere |
| Ephemeral database |
Fresh container per run |
Strongest isolation; highest startup cost |
For parallel suites, transaction rollback or unique namespacing usually wins. Clearing shared tables across workers causes cross-test data loss.
Determinism
Non-deterministic data is the most common source of flaky tests.
- Seed random generators explicitly and log the seed so a failure is reproducible.
- Inject the clock. Never call
now() inside code under test; pass a fixed instant.
- Avoid
today boundaries. A test that passes except near midnight or year end is already broken.
- Do not rely on insertion order. Assert with an explicit sort or compare as a set.
- Beware of locale and timezone. Fix them for the test run or assert locale-independent values.
Parallel safety
- Namespace every externally visible identifier with a worker or run token so two workers cannot collide on a unique constraint.
- Never share a mutable account, tenant, or queue between workers.
- Give each worker its own schema, database, or key prefix when the store does not support transactional isolation.
- Confirm cleanup runs even when a test fails, or a failure leaks state into later tests.
Anonymizing production data
Copying production data into a lower environment is a data-protection decision, not a convenience.
- Prefer synthetic data. If generated data can exercise the case, do not copy real records at all.
- Anonymize at export, never after load. A raw copy that lands in staging has already leaked.
- Masking is not anonymization. Replacing a name while keeping a rare postcode, birth date, and diagnosis still identifies the person.
- Preserve referential integrity and distribution. Transform consistently so joins still work and shapes stay realistic.
- Remove or tokenize direct identifiers, and reduce quasi-identifiers that re-identify in combination.
- Never copy secrets. Credentials, tokens, and keys must be regenerated, not masked.
- Record the legal basis and retention for any derived dataset, and expire it.
Verify the result: attempt re-identification on a sample before approving the dataset.
Gotchas
- Cleanup that only runs on success leaks state. Use fixtures or teardown hooks that always run.
- Cascading deletes can silently remove another test's data in a shared database.
- Auto-increment identifiers differ between local and CI, so never assert on a specific numeric id.
- A large seed dataset hides missing setup. Tests appear to pass because unrelated data happens to satisfy them.
- Anonymized data can still be unique. A single outlier value can identify a person even with names removed.
Output template
## Test data result
**Status:** implemented | improved | blocked
**Summary:** <what changed about data creation or isolation>
### Details
| Aspect | Approach |
| --- | --- |
| Creation | <factories, fixtures, or seed> |
| Isolation | <transaction, reset, namespace, or ephemeral> |
| Determinism | <clock injection, seeding, ordering> |
| Parallel safety | <namespacing and shared-resource handling> |
Production-derived data: <none, or anonymization method and legal basis>
### Validation
- Suite passes in parallel: <checked and result>
- Cleanup runs on failure: <checked and result>
Quality gate
References
1---2name: test-data-management3description: Build reliable test data with factories, fixtures, deterministic seeding, per-test isolation, and safe anonymization of production data for realistic datasets. Use when the user asks about test data, fixtures, factories, flaky tests caused by shared state, seeding a test database, parallel-safe data, or anonymizing production data for testing.4license: MIT5---67<!-- Generated from harness/github-copilot/skills/test-data-management/SKILL.md by harness/claude-code/scripts/convert_from_copilot.py. Edit the source, not this file. -->89# Test data management1011Give every test the data it needs, isolated from every other test, without copying sensitive production records into lower environments.1213## When to invoke1415- "Our tests fail when run in parallel."16- "Set up factories instead of these fixture files."17- "How do we get realistic data into staging?"18- "Tests pass alone but fail in the suite."19- "Anonymize this production dump for testing."2021## Factories over shared fixtures2223Shared fixture files create hidden coupling: one test's expectations depend on data another test also uses, so any change breaks unrelated tests.2425```python26# Factory: each test states only what it cares about27def make_order(**overrides):28 return Order(**{29 "id": uuid4(),30 "status": "pending",31 "total": Decimal("10.00"),32 "created_at": FIXED_CLOCK,33 **overrides,34 })3536def test_refund_rejected_when_already_refunded():37 order = make_order(status="refunded") # intent is visible in the test38```3940The test declares the one attribute that matters. Everything else is a sane default that can change without touching this test.4142## Isolation strategy4344Pick one and apply it consistently.4546| Strategy | How it works | Trade-off |47| --- | --- | --- |48| Transaction rollback | Wrap each test, roll back after | Fastest; cannot test committed behavior or multi-connection flows |49| Reset and reseed | Clear tables between tests, then reload a known baseline | Simple and thorough; slower as the schema grows |50| Unique namespacing | Every record uses a run-scoped identifier | Parallel-friendly; requires discipline everywhere |51| Ephemeral database | Fresh container per run | Strongest isolation; highest startup cost |5253For parallel suites, transaction rollback or unique namespacing usually wins. Clearing shared tables across workers causes cross-test data loss.5455## Determinism5657Non-deterministic data is the most common source of flaky tests.5859- **Seed random generators explicitly** and log the seed so a failure is reproducible.60- **Inject the clock.** Never call `now()` inside code under test; pass a fixed instant.61- **Avoid `today` boundaries.** A test that passes except near midnight or year end is already broken.62- **Do not rely on insertion order.** Assert with an explicit sort or compare as a set.63- **Beware of locale and timezone.** Fix them for the test run or assert locale-independent values.6465## Parallel safety6667- Namespace every externally visible identifier with a worker or run token so two workers cannot collide on a unique constraint.68- Never share a mutable account, tenant, or queue between workers.69- Give each worker its own schema, database, or key prefix when the store does not support transactional isolation.70- Confirm cleanup runs even when a test fails, or a failure leaks state into later tests.7172## Anonymizing production data7374Copying production data into a lower environment is a data-protection decision, not a convenience.7576- **Prefer synthetic data.** If generated data can exercise the case, do not copy real records at all.77- **Anonymize at export, never after load.** A raw copy that lands in staging has already leaked.78- **Masking is not anonymization.** Replacing a name while keeping a rare postcode, birth date, and diagnosis still identifies the person.79- **Preserve referential integrity and distribution.** Transform consistently so joins still work and shapes stay realistic.80- **Remove or tokenize direct identifiers**, and reduce quasi-identifiers that re-identify in combination.81- **Never copy secrets.** Credentials, tokens, and keys must be regenerated, not masked.82- **Record the legal basis and retention** for any derived dataset, and expire it.8384Verify the result: attempt re-identification on a sample before approving the dataset.8586## Gotchas8788- **Cleanup that only runs on success leaks state.** Use fixtures or teardown hooks that always run.89- **Cascading deletes can silently remove another test's data** in a shared database.90- **Auto-increment identifiers differ between local and CI**, so never assert on a specific numeric id.91- **A large seed dataset hides missing setup.** Tests appear to pass because unrelated data happens to satisfy them.92- **Anonymized data can still be unique.** A single outlier value can identify a person even with names removed.9394## Output template9596```markdown97## Test data result9899**Status:** implemented | improved | blocked100**Summary:** <what changed about data creation or isolation>101102### Details103| Aspect | Approach |104| --- | --- |105| Creation | <factories, fixtures, or seed> |106| Isolation | <transaction, reset, namespace, or ephemeral> |107| Determinism | <clock injection, seeding, ordering> |108| Parallel safety | <namespacing and shared-resource handling> |109110Production-derived data: <none, or anonymization method and legal basis>111112### Validation113- Suite passes in parallel: <checked and result>114- Cleanup runs on failure: <checked and result>115```116117## Quality gate118119- [ ] Tests declare only the data attributes they depend on.120- [ ] One isolation strategy is applied consistently across the suite.121- [ ] Clocks and random seeds are injected and reproducible.122- [ ] No assertion depends on generated identifiers or insertion order.123- [ ] Externally visible identifiers are namespaced for parallel runs.124- [ ] Cleanup executes even when a test fails.125- [ ] Any production-derived dataset was anonymized at export, verified against re-identification, and has a recorded legal basis and expiry.126- [ ] No credentials or secrets were copied from production.127128## References129130- [NIST SP 800-188: De-Identifying Government Datasets](https://csrc.nist.gov/pubs/sp/800/188/final)131- [GDPR Recital 26: anonymous information](https://gdpr-info.eu/recitals/no-26/)132- [Test Data Builder pattern](https://www.natpryce.com/articles/000714.html)