| name | test-data-factories |
| description | Generates realistic, maintainable test data with factories instead of brittle shared fixtures — factory libraries (Ruby factory_bot, Python factory_boy/model-bakery, PHP Foundry/Faker, JS Fishery/@mswjs/data/Fabbrica, Java instancio/Java-faker, Go fake) that build valid objects with sane defaults, Faker for realistic values, traits/transient params/variants for state, build vs create (in-memory vs persisted), sequences for unique fields, nested associations and object graphs without combinatorial fixtures, deterministic seeding (Faker.seed/locale pinning) for reproducible CI, idempotent upsert-based DB seeders for dev/E2E that re-run cleanly, and anonymized prod-like data via masking/synthesis — so every test declares only the fields it cares about and stays valid as the schema evolves. |
| when_to_use | Building test data — replacing shared YAML/SQL fixtures, generating valid model instances for unit/integration/E2E tests, seeding a dev or E2E database idempotently, creating object graphs with associations, or producing anonymized prod-like datasets. Distinct from write-tests (structures the assertions and suite; this generates the inputs they assert on) and validate-data-quality (checks real datasets for nulls/dupes/outliers; this manufactures synthetic data on purpose). |
When to Use
Reach for this skill when you need to manufacture valid, realistic test data and the pain is fixtures that rot or tests coupled to a giant shared dataset:
- "Replace our
fixtures/*.yml — every schema change breaks 200 tests"
- "Give me a valid User/Order with just the 2 fields this test cares about"
- "Build an order with 3 line items, a customer, and an address" (object graph)
- "Seed the dev/E2E database so
db:seed is safe to re-run"
- "Tests pass locally, flake in CI" (non-deterministic random data)
- "Generate realistic-but-fake names/emails/addresses" (Faker)
- "Make a prod-like dataset for staging without leaking PII" (anonymize)
NOT this skill:
- Structuring the assertions, arrange/act/assert, mocking, coverage, test naming → write-tests (it organizes the suite that consumes the data this skill builds)
- Checking a real dataset for nulls, dupes, outliers, schema drift → validate-data-quality (it inspects data you didn't generate; this fabricates data on purpose)
- Profiling/exploring an unfamiliar dataset's distributions → profile-dataset
- Generating inputs to find bugs by shrinking counterexamples → property-based-testing (it searches the input space; this hands you fixed, named, realistic instances)
- Driving a browser through the app to set up E2E state via the UI/API → write-playwright-e2e (it may call this skill's seeder to plant rows directly)
- Stabilizing a flaky test whose data was already non-deterministic → debug-flaky-tests (seed pinning here is one of its fixes)
- Safe schema changes / running the migration the seeder targets → db-migration-safety
- Designing the schema/associations themselves → design-relational-schema
Steps
-
Prefer factories over shared fixtures — fixtures are the anti-pattern you're replacing. A global users.yml/seed SQL becomes load-bearing: tests depend on user(:admin) having exactly these fields, so any edit ripples across the suite and tests silently couple to unrelated data ("mystery guest"). Factories invert this: a build(:user) is valid by default, and each test overrides only the attributes it asserts on. Pick the idiomatic library:
| Stack | Library | Build vs persist |
|---|
| Ruby / Rails | factory_bot | build(:user) (RAM) · create(:user) (DB) · build_stubbed (no DB, fake id) · attributes_for (hash) |
| Python / Django | factory_boy (DjangoModelFactory) or model-bakery (baker.make/baker.prepare) | .build() vs .create() / prepare vs make |
| Python (plain) | factory_boy Factory + faker | .build() only |
| JS / TS | Fishery (.build()), @mswjs/data, Fabbrica (Prisma), @faker-js/faker | build returns object; persist via your ORM |
| PHP / Laravel | Foundry or Eloquent factories + fakerphp/faker | Model::factory()->make() vs ->create() |
| Java / Kotlin | instancio, easy-random, datafaker (Java-faker successor) | POJO in memory |
| Go | go-faker/gofakeit + hand-rolled builder funcs | struct in memory |
-
Make the default object minimally valid; override per test. The factory's defaults must pass all model validations on their own so never fails for an unrelated reason. Then a test passes only what it cares about:
Common Errors
- Shared fixtures as source of truth. One
users.yml every test secretly depends on → mystery-guest coupling, schema edits break the world. Fix: factories with per-test overrides; delete the global fixture.
- Asserting against a Faker-generated value.
expect(user.name).to eq(faker_name) flakes the moment the seed changes. Fix: set asserted fields explicitly; Faker only fills don't-care fields.
- Non-deterministic data in CI with no seed. Intermittent failures no one can reproduce. Fix: pin
faker.seed/Faker.seed and the locale; print and replay the seed.
create everywhere when build would do. Hitting the DB (and its associations) for pure-logic tests makes the suite slow and order-dependent. Fix: build/build_stubbed/prepare for in-memory; create only when you query.
- Non-idempotent seed script. Blind
INSERT → second run violates UNIQUE / duplicates rows. Fix: find_or_create_by / ON CONFLICT DO UPDATE on a natural key; wrap in a transaction.
- Sub-factory explosion.
admin_user, suspended_admin_user, premium_suspended_admin_user… Fix: one base factory + composable traits.
- Over-deep association graphs. Every
create drags in 4 levels of records → slow tests and re-coupled data. Fix: build the minimal graph; stub the rest.
- Duplicate-key collisions from static defaults. A hardcoded
email: "a@b.com" default fails the second create. Fix: sequences or Faker::Internet.unique (or faker.string.uuid in @faker-js/faker, which dropped unique).
- Loading raw prod data into staging. Real PII in a lower environment = breach. Fix: deterministic anonymization/masking or synthesize; keep the mapping out of staging.
- Locale drift. Default Faker locale differs by machine/CI → address/phone format assertions break. Fix: pin the locale explicitly.
- Factories that drift from the schema. A new NOT NULL column makes every
create fail cryptically. Fix: FactoryBot.lint-style CI check that builds every factory.
Verify
- Factories are valid standalone: run the lint (
FactoryBot.lint / build-every-factory test) — every factory and trait builds and passes validations with zero overrides.
- No fixture coupling: grep the suite for the old shared fixture references; a test reads only the fields it sets/asserts, and editing an unrelated factory attribute breaks nothing.
- Determinism: run the suite twice with the same pinned seed → identical data and pass/fail; run with two different seeds → still green (no test asserts a Faker value).
- Uniqueness holds: create N rows from a factory with a UNIQUE column in a loop → no constraint violation (sequence/
unique/uuid working).
- Seed is idempotent: run
db:seed twice → identical row count and end state, no UNIQUE error; the second run is a no-op or clean upsert.
- build vs create honored:
build/build_stubbed issues zero SQL INSERTs (assert via query log/assert_no_queries); create persists exactly the intended graph.
- Traits compose:
create(:user, :admin, :suspended) yields both states; no combinatorial sub-factory needed.
- Object graph is minimal and correct: an order factory creates exactly its declared associations (customer + N items), foreign keys resolve, and no surprise extra records appear.
- Anonymized data is safe: spot-check the prod-like dataset — no real PII, the masking is deterministic (same input → same fake, FKs consistent), and date/format distributions are realistic.
Done = brittle shared fixtures are gone, each test declares only the fields it cares about against a valid-by-default factory, Faker fills the rest with a pinned seed+locale so CI is reproducible, object graphs and traits compose without sub-factory explosion, the dev/E2E seed is idempotent on a natural key, and any prod-like data is deterministically anonymized — all proven by the factory lint, the twice-with-same-seed run, and the double-seed idempotence check.