Entities and relations, once.
Fields, unique keys, belongsTo and hasMany live in one definition, validated before any test runs.
defineFixtures({ … })
v0.1.0 · deterministic, zero dependencies
Declare your entities and how they relate once. In each test, ask for the shape you need and get a complete, referentially consistent object graph, identical for the same seed.
npm install --save-dev fixturefloworganization_1Acmeuser_1 user_2 user_3Each carries organizationId: organization_1, with unique emails.
Every task points at its project. Assignees are drawn from the same org.
activeReal output from world.create(). Run it twice and the data is identical.
The idea
Hand-built objects repeat ids and foreign keys in every test, and drift out of sync. Fixtures move that wiring into one place.
Fields, unique keys, belongsTo and hasMany live in one definition, validated before any test runs.
defineFixtures({ … })
A with tree reshapes any level: counts, overrides and nested children.
world.create("organization", { with })
Own PRNG, a fixed clock and per-record streams. No Math.random, no Date.now.
Failures reproduce on any machine.
What you get
Ask for “an org with 2 users, one project with 5 tasks and a cancelled subscription”. Foreign keys, ids and “assignee belongs to the same org” come from the definitions.
Single, composite and foreign-key uniqueness, with deterministic regeneration of collisions and an error when an override causes one.
Missing targets and unbounded cycles are reported all at once when you define fixtures, not halfway through a test run.
Each record's stream comes from the seed plus its place in the graph, so editing one entity doesn't reshuffle unrelated data.
If anything in a create() throws, the world is left exactly as it was. Dangling foreign keys throw instead of slipping through.
No database, no runtime packages. Record types, foreign keys, with trees, overrides and scenarios are all inferred. ESM and CommonJS, Node ≥ 18.
Workflow
Create the world in beforeEach so tests are isolated. Give each file its own seed for different data, or share one for identical data.
Declare entities and how they relate, once, next to your models.
defineFixtures({ … })In each test, ask for the shape that matters and override only what is different.
world.create("organization")The graph is consistent and identical for the same seed, so failures reproduce.
expect(world.all("task"))Output depends on the seed, your definitions and the order of your calls. Read how determinism works →
How it works
Entities, unique keys and relations, with fields from seq, rng and now.
import { belongsTo, defineFixtures, entity, hasMany } from "fixtureflow";
export const fixtures = defineFixtures({
organization: entity({
fields: ({ seq, rng }) => ({ name: `Org ${seq}`, slug: `org-${rng.string(6)}` }),
unique: ["slug"],
hasMany: {
users: hasMany("user", { via: "organization", count: 3 }),
projects: hasMany("project", { via: "organization", count: 2 }),
subscriptions: hasMany("subscription", { via: "organization", count: 1 }),
},
}),
user: entity({
fields: ({ seq }) => ({ email: `user${seq}@example.test` }),
unique: ["email"],
belongsTo: { organization: belongsTo("organization") },
}),
project: entity({
fields: ({ seq }) => ({ title: `Project ${seq}` }),
belongsTo: { organization: belongsTo("organization") },
hasMany: { tasks: hasMany("task", { via: "project", count: [2, 4] }) },
}),
task: entity({
fields: ({ rng }) => ({ status: rng.pick(["todo", "doing", "done"] as const) }),
belongsTo: {
project: belongsTo("project"),
// an assignee picked from the users created in the same create() call
assignee: belongsTo("user", { optional: true, rate: 0.8, reuse: "scope" }),
},
}),
subscription: entity({
fields: () => ({ plan: "team" as "team" | "free", status: "active" as "active" | "cancelled" }),
belongsTo: { organization: belongsTo("organization") },
unique: ["organizationId"],
}),
});Works in Vitest, Jest and node:test. Override only what the test cares about.
import { beforeEach, expect, it } from "vitest";
import { fixtures } from "./fixtures";
let world: ReturnType<typeof fixtures.world>;
beforeEach(() => {
world = fixtures.world({ seed: "vitest" }); // isolated, reproducible
});
it("a cancelled team has no renewal", () => {
const org = world.create("organization", {
with: { subscriptions: { overrides: { status: "cancelled" } } },
});
const [subscription] = world.children("organization", org, "subscriptions");
expect(subscription?.status).toBe("cancelled");
});The core never touches a database. Rows come out parents first, batched by entity.
import { fixtures } from "./fixtures";
declare const db: { insert(table: string, rows: object[]): Promise<void> };
const world = fixtures.world({ seed: 7 });
world.create("organization");
// Rows arrive in creation order, so parents are always written before their children.
await world.persist(async (entity, rows) => {
await db.insert(entity, rows);
});Playground
This runs the fixtureflow source from this repository in your browser, against the definitions shown below. Nothing is mocked and nothing is uploaded.
organization_1Acmeuser_1user1@example.testuser_2user2@example.testuser_3user3@example.testsubscription_1teamproject_1Project 1task_1task_2task_3project_2Project 2task_4task_5task_6One create() call returned an organization, its users, projects, tasks and subscription, with every foreign key pointing at a record that exists. Assignees come from the same org. How determinism works →
import { fixtures } from "../fixtures";
export type Input = {
seed: string;
/** Overrides for the organization itself. */
overrides: Record<string, unknown>;
/** The `with` tree: reshape any level of the graph. */
tree: Record<string, unknown>;
};
/** Builds the graph twice, in two isolated worlds, to show the output is a pure function of the seed. */
export function generate({ seed, overrides, tree }: Input) {
const build = () => {
const world = fixtures.world({ seed });
const org = world.create("organization", { overrides, with: tree } as never);
return { world, org };
};
const { world, org } = build();
const reproducible = JSON.stringify(world.toJSON()) === JSON.stringify(build().world.toJSON());
const email = (id: string | null) => (id ? (world.get("user", id)?.email ?? null) : null);
return {
org,
reproducible,
stats: world.stats(),
tables: world.toJSON().tables,
users: world.children("organization", org, "users"),
subscriptions: world.children("organization", org, "subscriptions"),
projects: world.children("organization", org, "projects").map((project) => ({
project,
tasks: world.children("project", project, "tasks").map((task) => ({ task, assignee: email(task.assigneeId) })),
})),
};
}
export type Generated = ReturnType<typeof generate>;Building blocks
Everything after defineFixtures is type-checked: entity names, relation names, override keys and the shape of with.
belongsToMany-to-one. Required by default: a missing parent is generated. Optional with a rate.hasManyOne-to-many. count is a number or [min, max]; lazy children are generated on first load.uniqueSingle or composite keys. Collisions regenerate from a fresh deterministic stream.reusePick an existing parent from this create() call (scope) or the whole world instead of making one.scenariosNamed, typed presets. Plain functions over a world, so no business domain lives in the library.persistHands you rows in creation order, parents first, so you can insert them with any client.Documentation
More from M2Toolkit