v0.1.0 · deterministic, zero dependencies

Test data that fits together, every run.

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 fixtureflow
create("organization")13 recordsseed 42
  • organizationorganization_1Acme
  • 3 usersuser_1 user_2 user_3

    Each carries organizationId: organization_1, with unique emails.

  • 2 projects6 tasks

    Every task points at its project. Assignees are drawn from the same org.

  • 1 subscriptionactive

Real output from world.create(). Run it twice and the data is identical.

0runtime dependencies
Node ≥18ESM and CommonJS builds
~500krecords per second, measured on Node 22
1 seedone reproducible dataset

The idea

Say what is different about this test. Nothing else.

Hand-built objects repeat ids and foreign keys in every test, and drift out of sync. Fixtures move that wiring into one place.

Declare

Entities and relations, once.

Fields, unique keys, belongsTo and hasMany live in one definition, validated before any test runs.

defineFixtures({ … })

Request

A shape, not a pile of objects.

A with tree reshapes any level: counts, overrides and nested children.

world.create("organization", { with })

Replay

Same seed, same data.

Own PRNG, a fixed clock and per-record streams. No Math.random, no Date.now.

Failures reproduce on any machine.

What you get

Coherent graphs without a database or a pile of factories.

  • Whole graphs, not loose objects

    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.

  • Unique constraints

    Single, composite and foreign-key uniqueness, with deterministic regeneration of collisions and an error when an override causes one.

  • Definitions checked up front

    Missing targets and unbounded cycles are reported all at once when you define fixtures, not halfway through a test run.

  • Per-record random streams

    Each record's stream comes from the seed plus its place in the graph, so editing one entity doesn't reshuffle unrelated data.

  • Atomic creates

    If anything in a create() throws, the world is left exactly as it was. Dangling foreign keys throw instead of slipping through.

  • Zero dependencies, strict types

    No database, no runtime packages. Record types, foreign keys, with trees, overrides and scenarios are all inferred. ESM and CommonJS, Node ≥ 18.

Workflow

Define once. Create per test. Assert with confidence.

Create the world in beforeEach so tests are isolated. Give each file its own seed for different data, or share one for identical data.

Define

Declare entities and how they relate, once, next to your models.

  1. 1
    defineFixtures({ … })

Create

In each test, ask for the shape that matters and override only what is different.

  1. 2
    world.create("organization")

Assert

The graph is consistent and identical for the same seed, so failures reproduce.

  1. 3
    expect(world.all("task"))

Output depends on the seed, your definitions and the order of your calls. Read how determinism works →

How it works

From definitions, to a test, to a database.

  1. 1

    Define the schema

    Entities, unique keys and relations, with fields from seq, rng and now.

    ts
    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"],
      }),
    });
  2. 2

    Use it in a test

    Works in Vitest, Jest and node:test. Override only what the test cares about.

    ts
    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");
    });
  3. 3

    Persist, if you want

    The core never touches a database. Rows come out parents first, batched by entity.

    ts
    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

Describe the shape. Get the graph.

This runs the fixtureflow source from this repository in your browser, against the definitions shown below. Nothing is mocked and nothing is uploaded.

Reproducible13 records · 1 organization · 3 user · 2 project · 6 task · 1 subscription · built twice, identical output
  • organizationorganization_1Acmeorg-bprhls
    • useruser_1user1@example.test
    • useruser_2user2@example.test
    • useruser_3user3@example.test
    • subscriptionsubscription_1teamactive
    • projectproject_1Project 1
      • tasktask_1doingunassigned
      • tasktask_2todo→ user3@example.test
      • tasktask_3todo→ user2@example.test
    • projectproject_2Project 2
      • tasktask_4doing→ user1@example.test
      • tasktask_5todo→ user3@example.test
      • tasktask_6done→ user3@example.test

One 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 →

The code behind this panel
ts
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

A small vocabulary that covers the graph.

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.

See every relationship option →

Documentation

Everything is written down.

More from M2Toolkit