Skip to content

storymockBuild mocks from stories

Composable, type-safe builders for generating related mock data.

See it in action

Click ▶ Run to see example output. Click again for a different sample.

import { numeric, person, text, temporal, choice } from 'storymock';

const age    = numeric().min(18).max(65).create();     // 34
const name   = person().fullName().create();           // "Yuki Tanaka"
const id     = text().uuid().create();                 // "7b3e1d09-a4c2-..."
const born   = temporal().past(50).iso().create();     // "2001-03-14T08:23:41.000Z"
const status = choice('active', 'inactive').create();  // "active" or "inactive"

The three layers

Each layer builds on the one below. Use any layer on its own — or combine them for full relational datasets.

FakerGenerate a single value

numeric().min(1).max(100) · person().fullName() · text().uuid()

.min() · .max() · .precision() · .not() · .unique()

SchemaMap fields to fakers, define named states

schema<User>({ id: text().uuid(), name: person().fullName() }).trait('admin', { ... })

.trait() · when() · derive() · .id()

StoryCompose schemas, wire relationships

story().add('user', UserSchema).add('order', OrderSchema, { userId: ref('user') })

ref() · .setup() · .with() · .addMany()

Before & after

Without storymock

typescript
// Every test re-specifies the same states manually
const expiredAdmin = createUser({
  role: 'admin',
  status: 'active',
  subscription: {
    plan: 'enterprise',
    expiresAt: new Date('2024-01-01'),
    status: 'expired',
  },
});
const org = createOrg({ ownerId: expiredAdmin.id });
const team = createTeam({ orgId: org.id });
const members = Array.from({ length: 5 }, (_, i) =>
  createUser({
    teamId: team.id,
    role: i === 0 ? 'admin' : 'member',
  })
);
team.memberIds = members.map(m => m.id);
org.teamIds = [team.id];

// Next test — same setup, slightly different state.
// Copy, paste, tweak, hope nothing drifts.

With storymock

typescript
// Schemas define realistic defaults + named states once
const UserSchema = schema<User>({
  id: text().uuid(),
  name: person().fullName(),
  role: choice('admin', 'member'),
  subscription: SubscriptionSchema,
}).trait('expiredAdmin', {
  role: 'admin' as const,
  subscription: SubscriptionSchema.with('expired'),
});

// Stories compose objects and wire relationships
const orgStory = story()
  .add('owner', UserSchema.with('expiredAdmin'))
  .add('org', OrgSchema, { ownerId: ref('owner') })
  .addMany('members', UserSchema, 5)
  .setup(wireTeamMembers);

// Tests only express what's different
orgStory.create();
orgStory.with('owner', 'active').create();
orgStory.with('members[0]', 'admin').create();
  • Named states, not ad-hoc overrides

    Define 'expiredAdmin' or 'premium' once. Apply by name — no copy-pasting field values.

  • Realistic data without reinventing fakers

    person().fullName(), temporal().past(), finance().amount().precision(2) — built-in and composable.

  • Complex relationships in one call

    ref() wires foreign keys. .setup() distributes members. One .create() gives you the whole graph.