Building test factories with the builder pattern: createUser(), createOrder(), with overrides, traits, and sequences in TypeScript.
Building test factories with the builder pattern: createUser(), createOrder(), with overrides, traits, and sequences in TypeScript.
BeforeMerge offers hundreds of code review rules, guides, and detection patterns to help your team ship better code.
Test factories generate test data with sensible defaults. They eliminate boilerplate and make tests readable.
Without factories, every test creates data from scratch:
// Repetitive and fragile
const user = {
id: "123",
name: "Test User",
email: "test@example.com",
role: "member",
organizationId: "org-1",
createdAt: new Date(),
updatedAt: new Date(),
};If the User type changes, every test breaks.
let counter = 0;
function createUser(overrides: Partial<User> = {}): User {
counter++;
return {
id: `user-${counter}`,
name: `User ${counter}`,
email: `user-${counter}@test.com`,
role: "member",
organizationId: "org-1",
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
};
}
// Usage
const user = createUser();
const admin = createUser({ role: "admin" });
const alice = createUser({ name: "Alice", email: "alice@test.com" });function defineFactory<T>(defaults: () => T) {
return {
create(overrides: Partial<T> = {}): T {
return { ...defaults(), ...overrides };
},
createMany(count: number, overrides: Partial<T> = {}): T[] {
return Array.from({ length: count }, () => this.create(overrides));
},
};
}
const userFactory = defineFactory<User>(() => ({
id: crypto.randomUUID(),
name: "Test User",
email: `${crypto.randomUUID()}@test.com`,
role: "member",
organizationId: "org-1",
createdAt: new Date(),
updatedAt: new Date(),
}));
// Usage
const user = userFactory.create();
const admins = userFactory.createMany(5, { role: "admin" });Traits are named presets for common variations:
function defineFactory<T>(defaults: () => T) {
const traits: Record<string, Partial<T>> = {};
return {
trait(name: string, overrides: Partial<T>) {
traits[name] = overrides;
return this;
},
create(overridesOrTrait: Partial<T> | string = {}): T {
const traitOverrides = typeof overridesOrTrait === "string"
? traits[overridesOrTrait] || {}
: overridesOrTrait;
return { ...defaults(), ...traitOverrides };
},
createMany(count: number, overrides: Partial<T> | string = {}): T[] {
return Array.from({ length: count }, () => this.create(overrides));
},
};
}
const userFactory = defineFactory<User>(() => ({
id: crypto.randomUUID(),
name: "Test User",
email: `${crypto.randomUUID()}@test.com`,
role: "member",
organizationId: "org-1",
createdAt: new Date(),
updatedAt: new Date(),
}))
.trait("admin", { role: "admin" })
.trait("inactive", { role: "member", email: "inactive@test.com" });
// Usage
const admin = userFactory.create("admin");For integration tests, persist to the database:
async function createDbUser(overrides: Partial<User> = {}): Promise<User> {
const data = userFactory.create(overrides);
const { data: user } = await supabase
.from("users")
.insert(data)
.select()
.single();
return user!;
}
// Cleanup helper
async function cleanup(table: string, ids: string[]) {
await supabase.from(table).delete().in("id", ids);
}createUserWithOrg()