Files
magnus919_agent-skills/qa-methodology/references/test-data-management.md
T
usernameandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> 5bad13af56 feat(qa-methodology): refresh 5 traditional QA support references
- test-data-management: add time-travel testing (freezegun/timecop),
  data masking (static vs dynamic), GDPR/right-to-erasure, PII rules
- performance-testing: add tool landscape (k6/Locust/Gatling/JMeter),
  SLO-based threshold design, baseline-then-regress, CI cadence,
  gate-governance link to quality-gates-and-metrics
- security-testing: update to OWASP Top 10:2025 (A01-A10), add
  supply-chain/SBOM, STRIDE threat modeling, SAST/DAST/SCA landscape
- ci-failure-triage: add exit-code taxonomy (1/2/126/127/137/139/143),
  git bisect run, flake-vs-failure rerun-once protocol, numbered
  5-step diagnostic procedure
- test-debugging: add CI-vs-local divergence checklist (7 causes),
  test ordering/shared state (pytest-randomly), mock path binding

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-08-01 17:41:52 -04:00

188 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Test Data Management
## Fixtures vs Factories
| Approach | When | Trade-off |
|----------|------|-----------|
| Static fixtures (JSON/YAML files) | Small, stable datasets; API contract tests | Brittle to schema changes, easy to read |
| Factory functions (factory_boy, fishery) | Relational data, many-to-many, randomized | Setup complexity, harder to debug |
| Builder pattern | Complex objects with many optional fields | Verbose but explicit |
| Inline construction | One-off tests, 13 fields | Doesn't scale, but zero indirection |
### Decision: Fixture or Factory?
| Signal | Choose |
|--------|--------|
| Data shape is stable and shared across tests | Fixture |
| Tests need variations (different users, orders) | Factory |
| Schema changes frequently | Factory (adapts to model changes) |
| You need reproducible exact values | Fixture |
| You need randomized edge cases | Factory + Faker |
### Factory Pattern (Python)
```python
# factories.py
import factory
from myapp.models import User, Order
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
email = factory.Sequence(lambda n: f"user{n}@test.dev")
name = factory.Faker("name")
class OrderFactory(factory.django.DjangoModelFactory):
class Meta:
model = Order
user = factory.SubFactory(UserFactory)
total = factory.Faker("pydecimal", min_value=1, max_value=500, right_digits=2)
```
## Test Isolation
| Strategy | Mechanism | Speed | Safety |
|----------|-----------|-------|--------|
| Transaction rollback | Wrap test in transaction, rollback after | Fast | High — no cross-test leakage |
| Database per test | Create/drop schema per test | Slow | Highest — full isolation |
| Truncate between tests | `TRUNCATE ... CASCADE` after each | Medium | High |
| Unique prefixes | Each test uses `test-{uuid}-` prefixed data | Fast | Medium — relies on discipline |
**Rule:** Prefer transaction rollback (pytest-django `@pytest.mark.django_db`, Rails `use_transactional_tests`). Fall back to truncation only when tests need committed state (e.g., testing triggers, background jobs).
## Time-Travel Testing
Tests that depend on the current time are flaky by construction. Freeze or mock the clock; never `sleep()`.
| Tool | Language | Mechanism |
|------|----------|-----------|
| freezegun | Python | `@freeze_time("2025-06-15")` decorator; patches `datetime`, `time.time()` |
| timecop | Ruby | `Timecop.freeze(Time.local(2025, 6, 15))` with block form |
| jest.useFakeTimers | JavaScript/TS | `jest.setSystemTime(new Date('2025-06-15'))` |
| Clock interface (DI) | Any | Inject a `Clock` abstraction; tests supply a `FixedClock` |
### Worked Example (freezegun)
```python
from freezegun import freeze_time
@freeze_time("2025-06-15 12:00:00")
def test_subscription_expiry():
user = UserFactory(subscription_start=date(2025, 5, 15))
assert user.subscription_expired is True # 31 days > 30-day window
@freeze_time("2025-06-10 12:00:00")
def test_subscription_active():
user = UserFactory(subscription_start=date(2025, 5, 15))
assert user.subscription_expired is False # 26 days < 30-day window
```
### Gotcha: Timezone-Aware Freezing
freezegun defaults to UTC. If your app uses timezone-aware datetimes, specify the tz:
```python
@freeze_time("2025-06-15 12:00:00", tz_offset=0) # explicit UTC
```
Without this, `datetime.now()` and `datetime.utcnow()` can diverge by the server's local offset.
## Data Masking
When test environments need production-shaped data, mask it. Two approaches:
| Approach | How | When | Trade-off |
|----------|-----|------|-----------|
| **Static masking** | ETL pipeline copies production → staging with irreversible transforms | Periodic refresh (nightly/weekly) | Consistent snapshots; stale between refreshes |
| **Dynamic masking** | Query-time transformation; original data untouched | On-demand reads | Always fresh; query overhead; masking rules must cover every access path |
### Masking Rules
| Data Type | Mask Strategy | Example |
|-----------|--------------|---------|
| Email | Deterministic hash → `user{hash8}@test.dev` | `a3f9b2c1@test.dev` |
| Phone | Replace middle digits with `555-01xx` | `+1 (555) 0142` |
| Name | Faker substitution | `Jane Smith``Alice Johnson` |
| SSN/National ID | Replace with test-range values | `000-00-0000` (invalid range) |
| Address | Faker with same zip-code prefix | Preserves geo-distribution |
| Credit card | Luhn-valid test numbers | `4111 1111 1111 1111` |
**Deterministic masking** preserves referential integrity: the same real email always maps to the same masked email, so joins across tables remain valid.
## GDPR and Right-to-Erasure
Test data is not exempt from data protection regulations.
| Obligation | Test-Environment Implication |
|-----------|-----------------------------|
| Right to erasure (GDPR Art. 17) | Masked/synthetic data cannot be "erased" back to the original — verify masking is irreversible so erasure requests don't require test-data cleanup |
| Data minimization (GDPR Art. 5(1)(c)) | Tests should use the minimum fields needed; factories should not populate every column |
| Purpose limitation | Test data must not be reused for analytics, profiling, or any non-testing purpose |
| Breach notification | Test environments with masked PII-shaped data still need access controls; a leak of synthetic data that is indistinguishable from real data triggers the same response obligations |
### Compliance Checklist
- Masked data passes the same validation rules as real data (format, length, checksums)
- No reversible mapping exists between masked and original values
- Access to test environments is logged and role-controlled
- Data retention policies apply to test data (delete stale snapshots)
- Erasure requests are verified against test environments during quarterly audits
## PII Rules
> **Never use production PII in test databases.** This is the single most important test-data rule. Synthetic or masked data must be indistinguishable from real data in shape but contain zero real individuals' information.
| Rule | Rationale |
|------|-----------|
| No real names, emails, phones, addresses | GDPR, CCPA, and breach liability |
| Use obviously-fake credentials for auth tests | `AKIAIOSFODNN7EXAMPLE` (AWS example key) |
| If a test needs a specific edge case (unicode name, 255-char email), construct it explicitly | Random generation won't reliably hit edge cases |
| Rotate synthetic datasets quarterly | Prevents drift from schema changes |
### Synthetic Data Tools
| Tool | Use Case |
|------|----------|
| Faker | Names, emails, addresses, dates — realistic but fake |
| Presidio + Faker | Generate PII-shaped data that passes validation without real PII |
| SDV (Synthetic Data Vault) | Statistical replicas of production tables — preserves distributions |
| dbt seed + Jinja | Version-controlled CSV fixtures with templated expansion |
## External Service Data
| Service | Test Strategy |
|---------|---------------|
| Payment (Stripe) | Test-mode API keys + recorded fixtures (VCR.py / Polly.js) |
| Email (SendGrid) | Mock at transport layer; assert on message content |
| S3 / object storage | MinIO or `moto` (AWS mock); never hit real buckets |
| Third-party APIs | Contract tests (Pact) + recorded responses; rotate recordings quarterly |
## Data Volume Testing
| Scenario | Approach |
|----------|----------|
| Pagination | Seed exactly `page_size + 1` records |
| Performance under load | `generate_series()` in SQL or bulk factory (10K100K rows) |
| Edge cases | Empty table, single row, max-length fields, unicode, nulls |
| Time-dependent | Freeze time (freezegun, timecop) — never `sleep()` |
## Migration Testing
- Run migrations against a copy of production schema (anonymized) in CI
- Test both forward and rollback paths
- Data migrations: seed before-state, run migration, assert after-state
## Gotchas
- **Production PII in test environments** is a compliance violation, not a convenience. Automate masking in the CI/CD pipeline so no human copy step exists.
- **Shared mutable fixtures** across tests create ordering dependencies. Each test must construct its own data or use transaction rollback.
- **Time-dependent assertions without clock freezing** fail on timezone boundaries, DST transitions, and month-end dates. Always freeze.
- **Dynamic masking without coverage audit** leaks PII through unmasked access paths (views, materialized caches, API responses). Audit every query path.
## Related
- Test isolation and CI-vs-local divergence: [test-debugging.md](./test-debugging.md)
- Security test data rules: [security-testing.md](./security-testing.md)
*Sources: freezegun (GitHub, spulec), timecop (GitHub, travisjeffery), Faker (GitHub, joke2k), SDV (sdv.dev), GDPR Regulation (EU) 2016/679 Art. 5, 17, OWASP Testing Guide v4.2 (data masking), factory_boy (GitHub, FactoryBoy).*