Skip to content

Instantly share code, notes, and snippets.

@JonCatmull
Last active April 30, 2025 13:14
Show Gist options
  • Save JonCatmull/88bc8bbf96ea91592229dfc0aa27f416 to your computer and use it in GitHub Desktop.
Save JonCatmull/88bc8bbf96ea91592229dfc0aa27f416 to your computer and use it in GitHub Desktop.
Build mock data easily
import { describe, it, expect, beforeEach } from 'vitest';
import { createBuilder, Builder } from './create-builder.factory';
// Example interface for testing
interface Person {
id: number;
firstName: string;
lastName: string;
city?: string;
}
const defaultPerson: Person = {
id: 1,
firstName: 'John',
lastName: 'Doe',
city: 'Nowhere',
};
describe('createBuilder factory', () => {
let PersonBuilder: Builder<Person>;
beforeEach(() => {
PersonBuilder = createBuilder<Person>(defaultPerson);
});
it('should create a default object when no overrides are applied', () => {
const person = PersonBuilder.build();
expect(person).toEqual({
id: 1,
firstName: 'John',
lastName: 'Doe',
city: 'Nowhere',
});
});
it('should apply overrides using composition functions', () => {
const person = PersonBuilder.id(42).firstName('Tony').lastName('Stark').city('New York').build();
expect(person).toEqual({
id: 42,
firstName: 'Tony',
lastName: 'Stark',
city: 'New York',
});
});
it('should build many objects from an array of partial overrides', () => {
const people = PersonBuilder.buildMany([
{ id: 2, firstName: 'Jane' },
{ id: 3, lastName: 'Smith' },
]);
expect(people).toEqual([
{
id: 2,
firstName: 'Jane',
lastName: 'Doe', // default
city: 'Nowhere', // default
},
{
id: 3,
firstName: 'John', // default
lastName: 'Smith',
city: 'Nowhere', // default
},
]);
});
it('should build multiple identical objects when a number is passed to buildMany', () => {
const people = PersonBuilder.city('New York').buildMany(3);
// Expect 3 copies of the default object:
expect(people).toHaveLength(3);
people.forEach((person) => {
expect(person).toEqual({
id: 1,
firstName: 'John', // default
lastName: 'Doe', // default
city: 'New York',
});
});
});
it('should not mutate the builder’s default after calling build', () => {
const personA = PersonBuilder.id(101).build();
// Building a new person from the same builder to see if id(101) affected it
const personB = PersonBuilder.build();
expect(personA.id).toBe(101);
// personB should remain the default
expect(personB.id).toBe(1);
});
it('should not mutate the builder’s default after calling buildMany', () => {
const peopleA = PersonBuilder.buildMany([{ id: 101 }, { id: 102 }]);
// Building new people from the same builder to see if buildMany(2) affected it
const peopleB = PersonBuilder.buildMany(2);
expect(peopleA[0].id).toBe(101);
expect(peopleA[1].id).toBe(102);
// peopleB should copy the default
expect(peopleB[0].id).toBe(1);
expect(peopleB[1].id).toBe(1);
});
});
/**
* A `CompositionFunction` creates or updates one specific property on
* your mock data object. It returns a new `Builder` instance with that
* updated property value.
*
* @example
* // Suppose we have a builder for a User object
* // The `.username('SteveRogers')` is a composition function call:
* const steve = UserBuilder.username('SteveRogers').build();
*/
export type CompositionFunction<T, K extends keyof T> = (value: T[K]) => Builder<T>;
/**
* `CompositionFunctions<T>` is a dictionary of composition functions
* (one per property in `T`).
*
* Example: If T = { id: number; username: string },
* this type has `id(value: number): Builder<T>` and `username(value: string): Builder<T>`.
*/
export type CompositionFunctions<T> = Required<{
[P in keyof T]: CompositionFunction<T, P>;
}>;
/**
* A helper type for the `buildMany` function, which can create an array of objects
* either from an array of partial overrides or by a numeric count of duplicates.
*/
export type BuildManyFunction<T> = (arg: Partial<T>[] | number) => T[];
/**
* A `Builder<T>` includes:
* - One composition function per property in `T`
* - A `_obj` property containing the current base/default object
* - A `.build()` method returning a finalized, read-only object
* - A `.buildMany()` method to produce an array of objects in bulk
*/
export type Builder<T> = CompositionFunctions<T> & {
/**
* Read-only property with the “current” object state.
*/
_obj: Readonly<T>;
/**
* Builds a single object from the current default/base values
* plus any overridden properties set via composition functions.
*
* @returns A read-only copy of the final object.
*/
build(): Readonly<T>;
/**
* Builds an array of objects.
* - Pass an array of partial overrides to merge with the default/base object
* - Pass a number N to create N identical copies of the default/base object
*
* @example
* const multipleUsers = UserBuilder.buildMany([
* { username: 'BruceBanner' },
* { username: 'NatashaRomanoff' }
* ]);
*/
buildMany: BuildManyFunction<T>;
};
/**
* `CreateBuilderFunction` is a function type that takes a default value `T`
* and returns a `Builder<T>`.
*
* @example
* // Usage:
* export const UserBuilder: Builder<User> = createBuilder<User>(myDefaultUser);
*/
export type CreateBuilderFunction = <T>(defaultValue: T) => Builder<T>;
/**
* Creates a composition function for a specific property (keyName) on `obj`.
* When invoked, it sets that property to the provided `value` and returns a new `Builder`.
*
* @param builderFunction The top-level factory function (createBuilder).
* @param obj The current mock data object.
* @param keyName The property name to set (e.g., 'username').
*/
function createCompositionFunction<T, K extends keyof T>(
builderFunction: CreateBuilderFunction,
obj: T,
keyName: K
): CompositionFunction<T, K> {
return (value: T[K]) => builderFunction<T>({ ...obj, [keyName]: value });
}
/**
* Dynamically creates a dictionary of composition functions for each property in `obj`.
* For every key in `obj`, we create a function that sets that key and returns a new builder.
*
* @param builderFunction The top-level factory function (createBuilder).
* @param obj The current mock data object.
* @returns A `CompositionFunctions<T>` object where each property is a function.
*/
function createCompositionFunctions<T extends {}>(
builderFunction: CreateBuilderFunction,
obj: T
): CompositionFunctions<T> {
const compositionFuncs: Partial<CompositionFunctions<T>> = {};
// We use (keyof T)[] to assure TS that every key is truly a key of T
for (const key of Object.keys(obj) as (keyof T)[]) {
compositionFuncs[key] = createCompositionFunction(builderFunction, obj, key);
}
return compositionFuncs as CompositionFunctions<T>;
}
/**
* Creates a `.buildMany()` function for a given object. It can either:
* - Take an array of partial overrides and return an array of merged objects
* - Take a number N and return an array of N identical copies of the object
*
* @param obj The current mock data object.
* @returns A function that implements `.buildMany()`.
*/
function createBuildManyFunction<T>(obj: T): BuildManyFunction<T> {
return (arg: Partial<T>[] | number) => {
if (Array.isArray(arg)) {
return arg.map((partialObj) => ({
...obj,
...partialObj,
}));
}
// If arg is a number, return an array with `arg` copies of the object
return Array(arg).fill({ ...obj });
};
}
/**
* The main builder factory. Given a default object of type `T`,
* it returns a `Builder<T>` with composition methods for each property,
* plus `.build()` and `.buildMany()`.
*
* @param defaultValue The base/default object for your mock data.
*
* @example
* // Example usage with the `User` interface:
* import { User } from './user.interface';
*
* const defaultUser: User = {
* id: 3000,
* username: 'TonyStark',
* email: '[email protected]',
* roleIds: [999],
* roleId: '999',
* depotIds: ['STARK_TOWER'],
* sso: false,
* };
*
* export const UserBuilder: Builder<User> = createBuilder<User>(defaultUser);
*
* // Then in tests:
* const singleUser = UserBuilder.username('SteveRogers').build();
* const multipleUsers = UserBuilder.buildMany([
* { username: 'BruceBanner' },
* { username: 'NatashaRomanoff', email: '[email protected]' },
* ]);
*/
export const createBuilder: CreateBuilderFunction = <T>(defaultValue: T): Builder<T> => {
// Make a read-only copy of the default object
const _obj: Readonly<T> = defaultValue;
return {
_obj,
build: () => _obj,
buildMany: createBuildManyFunction(_obj),
...createCompositionFunctions(createBuilder, _obj),
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment