Test Factory Patterns
Test factories generate test data with sensible defaults. They eliminate boilerplate and make tests readable.
The Problem
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.
Basic Factory
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" });
Generic Factory Builder
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
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");
Database Factories
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);
}
Best Practices
- Use unique values (UUID, counter) to prevent collisions
- Keep defaults minimal and valid
- Use overrides for test-specific values
- Create related data together:
createUserWithOrg()
- Clean up database records after each test