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.

  • Testing & QA
  • $test-data-factories
  • 192 lines
  • 2 files
  • ~1,900 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- test-data-factories

Installs to ~/.agents/skills/test-data-factories/ for every project on this machine.

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

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.

~/.agents/skills/test-data-factories/SKILL.md

--- name: test-data-factories description: Designs test data factories and builders - factory_boy for Python and Django, fishery with @faker-js/faker for TypeScript, FactoryBot for Ruby, test… license: MIT ---

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

  1. 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.
  2. Read the domain model and validation rules (ORM models, Zod/Pydantic schemas, DB constraints) so defaults satisfy them.
  3. Look for shared fixture files (fixtures/*.json, seed.sql, giant conftest.py objects). Note which tests mutate them.
  4. Check how the suite isolates the database; factories that persist rely on it (see integration-testing).

Choose the tool

StackDefaultNotes
Python, Django, SQLAlchemyfactory_boyDjangoModelFactory, SQLAlchemyModelFactory; factory.Faker wraps Faker
TypeScript / JavaScriptfishery + @faker-js/fakerTyped Factory.define<T>, build/buildList, async create via onCreate
Ruby, RailsFactoryBotDetailed RSpec usage lives in rspec-testing
Java, Kotlin, C#Hand-written test data buildersFluent aUser().withEmail(..).build(); Kotlin data class copy() often suffices
GoConstructor with functional options or a struct literal helperNewTestUser(t, WithRole("admin"))
Tiny project, 2-3 modelsPlain helper functions with keyword overridesA 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.test never collides and is readable in failure output. Use the reserved .test or example.com domains.
  • 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 Order should not create a Store, a Merchant, three Products and a TaxRegion unless 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

NeedUse
Pure function, validation, serializer testbuild (in memory)
Query, constraint, repository testcreate (persisted)
Many rows for a list or pagination testcreate_batch / createList with explicit size; keep it small (3-25)
Big volume for performance testsBulk insert or a seed script, not factories (see database-seeding)

Migrating from shared fixtures

  1. Pick the most-used fixture object; write a factory whose defaults match it.
  2. Replace usages test by test, moving the fields each test relies on into the call site.
  3. Run the suite after each batch; a test that now fails was depending on a hidden value. Make that value explicit.
  4. 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_create and similar hide bugs where code should create a row but the factory already did.
  • Factories that call application services (sending email, charging cards) inside create make 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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem