Test Data Factories skill for Codex
Valid by default, special only where it matters. Codex replaces copy-pasted object literals and brittle shared JSON fixtures with factories whose defaults are minimal, valid and collision-free. Each test then states only the fields it asserts on, randomness is seeded, associations stay shallow, and in-memory builds are preferred over database writes so suites get faster rather than slower as they grow.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- test-data-factories Installs to ~/.agents/skills/test-data-factories/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install test-data-factories The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project test-data-factories Installs to .agents/skills/test-data-factories/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/test-data-factories.zipunzip -o test-data-factories.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) test-data-factories PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $test-data-factories, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs test data factories and builders - factory_boy for Python and Django, fishery with @faker-js/faker for TypeScript, FactoryBot for Ruby, test data builders for Java, Kotlin and C#, functional options for Go - with minimal valid defaults, sequences for unique fields, traits, explicit associations, build versus create, and seeded randomness. Use when tests repeat long object literals, fixtures are shared JSON blobs that break on schema changes, tests collide on unique constraints, or a suite is slow because factories create whole object graphs.
Use it when
- Tests repeat long object literals or constructor calls
- Shared fixture files break many tests on every schema change
- Parallel tests collide on unique constraints
- Factories create so many rows that the suite is slow
Use something else when
- You need realistic seed data for a dev or demo database: use database-seeding
- You need large synthetic datasets for ML or analytics: use synthetic-data-generation
- The question is RSpec structure and FactoryBot in Rails specs specifically: use rspec-testing
How to check the result
Every factory builds with no arguments in a smoke test, the suite passes in random order with parallel workers, and representative tests create fewer rows than before.
The mistake it prevents
Letting tests assert on values that came from factory defaults or Faker, so a harmless default change breaks unrelated tests.
A good factory makes the default object valid and boring, lets each test state only the fields that matter to its assertion, never collides on unique keys, and creates the fewest database rows possible. Reading a test should tell you why its data is special.
Inspect first
- Read
AGENTS.md, then find existing helpers:rg -n "factory|Factory|build\(|create\(|fixture" tests spec test -g '!node_modules'. Extend the project’s existing library instead of adding a second one. - Read the domain model and validation rules (ORM models, Zod/Pydantic schemas, DB constraints) so defaults satisfy them.
- Look for shared fixture files (
fixtures/*.json,seed.sql, giantconftest.pyobjects). Note which tests mutate them. - Check how the suite isolates the database; factories that persist rely on it
(see
integration-testing).
Choose the tool
| Stack | Default | Notes |
|---|---|---|
| Python, Django, SQLAlchemy | factory_boy | DjangoModelFactory, SQLAlchemyModelFactory; factory.Faker wraps Faker |
| TypeScript / JavaScript | fishery + @faker-js/faker | Typed Factory.define<T>, build/buildList, async create via onCreate |
| Ruby, Rails | FactoryBot | Detailed RSpec usage lives in rspec-testing |
| Java, Kotlin, C# | Hand-written test data builders | Fluent aUser().withEmail(..).build(); Kotlin data class copy() often suffices |
| Go | Constructor with functional options or a struct literal helper | NewTestUser(t, WithRole("admin")) |
| Tiny project, 2-3 models | Plain helper functions with keyword overrides | A library is overhead until the third model |
Rules for defaults
- Minimal and valid. Fill required fields only. Optional fields stay empty unless the domain makes “empty” unusual.
- Unique via sequences, not random.
user-{n}@example.testnever collides and is readable in failure output. Use the reserved.testorexample.comdomains. - Nothing a test asserts on comes from a default. If a test checks the email, it sets the email.
- Seed any randomness. Random-but-valid data is fine for fields nobody asserts on, only with a fixed seed so failures replay.
- Time is explicit. Default timestamps to a fixed instant, not
now(), unless the test freezes the clock. - Associations are lazy and shallow. Creating an
Ordershould not create aStore, aMerchant, threeProductsand aTaxRegionunless the test needs them. - Traits for named states (
admin,suspended,paid), not boolean soup in every test.
Python: factory_boy
import factory
from datetime import datetime, timezone
from myapp.models import User, Order
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
django_get_or_create = ("email",) # only if tests rely on idempotent creation
email = factory.Sequence(lambda n: f"user-{n}@example.test")
name = factory.Faker("name")
is_active = True
created_at = datetime(2025, 1, 15, 12, 0, tzinfo=timezone.utc)
class Params:
suspended = factory.Trait(is_active=False, suspended_reason="chargeback")
class OrderFactory(factory.django.DjangoModelFactory):
class Meta:
model = Order
customer = factory.SubFactory(UserFactory)
total_cents = 2500
currency = "USD"
reference = factory.LazyAttribute(lambda o: f"ORD-{o.customer.pk:06d}")
def test_suspended_users_cannot_order(db):
user = UserFactory(suspended=True)
with pytest.raises(AccountSuspended):
place_order(user, OrderFactory.build(customer=user)) # build: no DB row
Seed Faker once per session in conftest.py:
factory.random.reseed_random(1234) (also seeds the wrapped Faker).
TypeScript: fishery
import { Factory } from "fishery";
import { faker } from "@faker-js/faker";
import type { User, Order } from "../src/types";
faker.seed(1234);
export const userFactory = Factory.define<User>(({ sequence }) => ({
id: `usr_${sequence}`,
email: `user-${sequence}@example.test`,
name: faker.person.fullName(),
role: "member",
createdAt: new Date("2025-01-15T12:00:00Z"),
}));
export const adminFactory = userFactory.params({ role: "admin" });
export const orderFactory = Factory.define<Order>(({ sequence, associations }) => ({
id: `ord_${sequence}`,
customer: associations.customer ?? userFactory.build(),
totalCents: 2500,
currency: "USD",
}));
Persisting: add onCreate(async (attrs) => db.insert(...)) and call await factory.create().
Keep build() as the default in unit tests.
Java/Kotlin: test data builder
public final class UserBuilder {
private static final AtomicInteger SEQ = new AtomicInteger();
private String email = "user-" + SEQ.incrementAndGet() + "@example.test";
private Role role = Role.MEMBER;
private Instant createdAt = Instant.parse("2025-01-15T12:00:00Z");
public static UserBuilder aUser() { return new UserBuilder(); }
public UserBuilder withEmail(String email) { this.email = email; return this; }
public UserBuilder asAdmin() { this.role = Role.ADMIN; return this; }
public User build() { return new User(email, role, createdAt); }
}
In Kotlin, prefer a function with default arguments:
fun aUser(email: String = nextEmail(), role: Role = Role.MEMBER) = User(email, role, FIXED_NOW).
Go: functional options
func NewTestUser(t *testing.T, opts ...func(*User)) User {
t.Helper()
n := seq.Add(1)
u := User{Email: fmt.Sprintf("user-%d@example.test", n), Role: "member", CreatedAt: fixedNow}
for _, o := range opts {
o(&u)
}
return u
}
func WithRole(r string) func(*User) { return func(u *User) { u.Role = r } }
Build versus create
| Need | Use |
|---|---|
| Pure function, validation, serializer test | build (in memory) |
| Query, constraint, repository test | create (persisted) |
| Many rows for a list or pagination test | create_batch / createList with explicit size; keep it small (3-25) |
| Big volume for performance tests | Bulk insert or a seed script, not factories (see database-seeding) |
Migrating from shared fixtures
- Pick the most-used fixture object; write a factory whose defaults match it.
- Replace usages test by test, moving the fields each test relies on into the call site.
- Run the suite after each batch; a test that now fails was depending on a hidden value. Make that value explicit.
- Delete the fixture when its last reference is gone.
Verification
- Every factory builds and creates successfully with no arguments; add one smoke test per
factory (
UserFactory(),userFactory.build()) so schema changes fail in one place. - Run the suite in random order and in parallel; unique-constraint errors mean a default is not sequenced.
- Count queries or rows for a representative test before and after; deep association chains show up immediately.
Gotchas
factory.Faker("email")produces duplicates over a long suite; sequences do not.- Sequences are per process. With pytest-xdist or Jest workers, add the worker id to unique values or isolate databases per worker.
django_get_or_createand similar hide bugs where code should create a row but the factory already did.- Factories that call application services (sending email, charging cards) inside
createmake tests slow and dangerous. Factories write data; tests call services. - A factory default that satisfies today’s validation but not tomorrow’s will break dozens of tests at once. That is the point: fix the factory, not each test.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Test Data Factories"
short_description: "Valid by default, special only where it matters"
default_prompt: "$test-data-factories Replace the giant fixtures.json in our tests with factories" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.