mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-19 07:27:04 +03:00
- 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>
188 lines
8.7 KiB
Markdown
188 lines
8.7 KiB
Markdown
# 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, 1–3 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 (10K–100K 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).*
|