Test Factory Patterns
Build maintainable test data using factory functions, builders, and faker.js
When to Use
- Creating test data for unit and integration tests
- Reducing duplication in test setup across test files
- Generating realistic fake data for testing edge cases
- Building complex object graphs with sensible defaults
Instructions
- Simple factory function with defaults and overrides:
function createUser(overrides?: Partial<User>): User {
return {
id: crypto.randomUUID(),
name: 'Test User',
email: `user-${crypto.randomUUID().slice(0, 8)}@test.com`,
role: 'user',
createdAt: new Date(),
...overrides,
};
}
// Usage
const admin = createUser({ role: 'admin', name: 'Admin' });
const alice = createUser({ name: 'Alice' });
- Factory with faker.js for realistic data:
import { faker } from '@faker-js/faker';
function createUser(overrides?: Partial<User>): User {
return {
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
role: 'user',
avatar: faker.image.avatar(),
createdAt: faker.date.past(),
...overrides,
};
}
- Builder pattern for complex objects:
class UserBuilder {
private data: Partial<User> = {};
withName(name: string): this {
this.data.name = name;
return this;
}
withRole(role: Role): this {
this.data.role = role;
return this;
}
withEmail(email: string): this {
this.data.email = email;
return this;
}
asAdmin(): this {
this.data.role = 'admin';
return this;
}
build(): User {
return createUser(this.data);
}
}
// Usage
const admin = new UserBuilder().withName('Alice').asAdmin().build();
- Factory for related entities:
function createPost(
overrides?: Partial<Post> & { author?: Partial<User> }
): Post & { author: User } {
const author = createUser(overrides?.author);
return {
id: crypto.randomUUID(),
title: faker.lorem.sentence(),
content: faker.lorem.paragraphs(3),
authorId: author.id,
published: false,
createdAt: new Date(),
...overrides,
author,
};
}
- Batch factory for lists:
function createUsers(count: number, overrides?: Partial<User>): User[] {
return Array.from({ length: count }, () => createUser(overrides));
}
const tenAdmins = createUsers(10, { role: 'admin' });
- Deterministic factories for snapshot testing:
faker.seed(42); // Same seed = same data every run
function createDeterministicUser(): User {
return createUser(); // Always generates the same sequence
}
- Database factory for integration tests:
async function createDbUser(prisma: PrismaClient, overrides?: Partial<User>) {
const data = createUser(overrides);
return prisma.user.create({
data: {
email: data.email,
name: data.name,
role: data.role,
},
});
}
// In tests
const user = await createDbUser(prisma, { role: 'admin' });
- Organize factories in a central file:
// test/factories/index.ts
export { createUser, createUsers, UserBuilder } from './user-factory';
export { createPost, createPosts } from './post-factory';
export { createOrder } from './order-factory';
Details
Test factories centralize test data creation, providing consistent defaults while allowing per-test customization. They eliminate the "copy-paste test setup" anti-pattern.
Factory vs fixture vs seed:
- Factory — creates data programmatically with overrides. Best for unit and integration tests
- Fixture — static data loaded from files (JSON, SQL). Best for large, stable datasets
- Seed — populates a database for development. Factories can be used inside seeds
Faker.js tips:
faker.seed(n)makes all subsequent calls deterministic — use for reproducible testsfaker.helpers.arrayElement(['a', 'b', 'c'])picks randomly from a listfaker.helpers.multiple(fn, { count: 5 })generates multiple values
Factory design principles:
- Default values should produce a valid object (passes all validation)
- Override only what matters for the test — defaults handle the rest
- Unique values (email, ID) should be generated, not hardcoded — prevents constraint violations
- Related objects should be creatable together (post + author)
Trade-offs:
- Factories reduce test setup duplication — but add a maintenance surface (factory changes can break many tests)
- Faker generates realistic data — but can produce edge cases unexpectedly (very long names, unicode characters)
- Builders are expressive — but verbose for simple objects. Use plain factories for objects with fewer than 5 fields
- Deterministic seeds help snapshots — but hide randomized testing benefits
Source
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.