Every codebase starts with good intentions.
You write a few test fixtures. A few Storybook mocks. Maybe a factory or two.
Then six months later:
- Your schema has changed 14 times.
- Half your fixtures are out of date.
- Tests fail because mock data is invalid.
- Storybook examples no longer reflect reality.
- Someone spends an hour debugging a test only to discover the fixture was wrong.
We've accepted this as normal.
I don't think it should be.
That's why I built fixture-gen — a deterministic fixture compiler that generates realistic, schema-valid test data directly from the contracts you already maintain.
Instead of manually creating and maintaining fixtures, you point fixture-gen at your schema and get back valid, reproducible data instantly.
// Before
const user = {
id: "abc",
name: "John",
email: "john@example.com",
}
// After
const user = generate(UserSchema)
Simple idea. Surprisingly powerful.
The hidden cost of fixture maintenance
Most teams already have a source of truth:
- Zod schemas
- Valibot schemas
- ArkType definitions
- TypeBox contracts
But we still maintain a completely separate layer of mock data.
That means every schema change creates another maintenance task:
- Add a required field? Update fixtures.
- Change validation constraints? Update fixtures.
- Add relationships between entities? Update fixtures.
Eventually your fixtures become their own system that needs maintenance.
The more I thought about it, the stranger it seemed.
If the schema already knows what valid data looks like, why are we manually recreating that knowledge somewhere else?
Meet fixture-gen
fixture-gen generates realistic test data from existing schemas.
It supports:
- Zod
- Valibot
- ArkType
- TypeBox
Because it works through the Standard Schema ecosystem, you don't need adapters, converters, or custom integration layers.
Here's a basic example:
import { generate } from "fixture-gen"
import { z } from "zod"
const User = z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
age: z.number().int().min(18).max(99),
})
const user = generate(User, {
seed: 42,
})
Output:
{
id: "1f8c...",
name: "Felicia Bartell",
email: "orval.kihn@example.com",
age: 37
}
The generated values satisfy the schema constraints automatically.
No factory code.
No mock builders.
No copy-pasted fixture objects.
Deterministic by default
One thing that has always bothered me about random data generators is that they make debugging harder.
A test passes locally.
Fails in CI.
You rerun it.
Now it passes.
Good luck reproducing that.
fixture-gen uses seeded generation.
const a = generate(User, { seed: 7 })
const b = generate(User, { seed: 7 })
// identical
expect(a).toEqual(b)
Change the seed and you'll get different data.
Keep the seed and you'll get identical output across runs, machines, and CI environments.
This turns out to be incredibly useful for:
- Snapshot testing
- Visual regression testing
- Storybook stories
- Reproducible bug reports
- CI stability
The feature I wanted most: relational fixtures
Most fixture generators stop at individual records.
Real applications don't.
Users have posts.
Customers have invoices.
Organizations have members.
Orders have line items.
The moment relationships enter the picture, fixture generation usually becomes manual again.
fixture-gen can generate linked datasets with real foreign-key relationships.
const { users, posts } = generateRelational(
{
users: User,
posts: Post,
},
{
seed: 42,
counts: {
users: 3,
posts: 10,
},
relations: {
"posts.userId": "users.id",
},
}
)
Every generated post references a real generated user.
No additional wiring required.
For integration tests, local development databases, and Storybook scenarios, this saves a surprising amount of time.
Scenarios > randomness
Another thing I've grown to dislike is fixture code that hides intent.
Consider these two examples:
const user = generate(User)
versus
const user = generate(User, {
scenario: "boundary-max",
})
The second one immediately tells you why the fixture exists.
fixture-gen includes built-in scenarios such as:
- happy-path
- empty-state
- boundary-min
- boundary-max
- invalid
- missing-subtree
You can also create your own:
defineScenario("admin-user", {
role: "admin",
active: true,
})
Now your test data communicates intent instead of just existing.
Real-world usage
React component tests
test("renders user name", () => {
const user = generate(UserSchema, {
seed: 1,
})
render(<UserCard user={user} />)
expect(screen.getByText(user.name))
.toBeInTheDocument()
})
API tests
const payload = generate(UserSchema)
await createUser(payload)
Storybook
export const Admin = {
args: {
user: generate(UserSchema, {
overrides: {
role: "admin",
},
}),
},
}
The common theme is simple:
Generate valid data first.
Customize only the parts that matter.
Why I built it
Most existing solutions solve only part of the problem.
Some generate realistic values but know nothing about your schema.
Others understand schemas but are tightly coupled to a specific validation library.
I wanted something that:
- Worked across schema libraries
- Produced deterministic output
- Generated relational datasets
- Supported real-world constraints
- Reduced fixture maintenance to near zero
So I built it.
What surprised me most
The biggest benefit wasn't writing less fixture code.
It was trusting fixtures again.
When test data comes directly from the same schema your application validates against, an entire category of bugs disappears.
You stop asking:
"Is the test failing because the application is broken or because the fixture is wrong?"
That's a bigger productivity win than I expected.
Give it a try
If you're already using Zod, Valibot, ArkType, or TypeBox, you probably have everything you need to start generating fixtures today.
I'd love feedback from developers using it in real projects:
- What schemas break?
- What generators are missing?
- What workflows should be supported?
- What integrations would be most useful?
If the project looks useful, give it a try, open an issue, contribute a PR, or star the repository.
And if you're still maintaining hundreds of hand-written fixture objects, maybe it's time to let your schemas do the work.
Top comments (0)