Test Data Factory Builder
Create composable factories for consistent test data.
Factory Pattern
// factories/UserFactory.ts import { faker } from "@faker-js/faker";
export class UserFactory { private data: Partial<User> = {};
static create(overrides?: Partial<User>): User { return new UserFactory().with(overrides).build(); }
with(overrides: Partial<User>): this { this.data = { ...this.data, ...overrides }; return this; }
withEmail(email: string): this { this.data.email = email; return this; }
withRole(role: UserRole): this { this.data.role = role; return this; }
asAdmin(): this { this.data.role = "ADMIN"; return this; }
build(): User { return { id: this.data.id || faker.string.uuid(), email: this.data.email || faker.internet.email(), name: this.data.name || faker.person.fullName(), role: this.data.role || "USER", createdAt: this.data.createdAt || faker.date.past(), ...this.data, }; } }
// Usage const user = UserFactory.create(); const admin = UserFactory.create().asAdmin().build(); const specific = UserFactory.create({ email: "test@example.com" });
Builder Pattern
// builders/OrderBuilder.ts export class OrderBuilder { private user?: User; private items: OrderItem[] = []; private status: OrderStatus = "PENDING";
forUser(user: User): this { this.user = user; return this; }
withItem(product: Product, quantity: number = 1): this { this.items.push({ id: faker.string.uuid(), productId: product.id, quantity, price: product.price, }); return this; }
withStatus(status: OrderStatus): this { this.status = status; return this; }
asPaid(): this { this.status = "PAID"; return this; }
async build(): Promise<Order> { if (!this.user) { throw new Error("User is required"); }
const total = this.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return {
id: faker.string.uuid(),
userId: this.user.id,
items: this.items,
total,
status: this.status,
createdAt: new Date(),
};
} }
// Usage const order = await new OrderBuilder() .forUser(user) .withItem(laptop, 2) .withItem(phone, 1) .asPaid() .build();
Relationship Handling
// factories/OrderFactory.ts export class OrderFactory { static async createWithUser(overrides?: Partial<Order>): Promise<Order> { // Create user if not provided const user = UserFactory.create();
// Create products
const products = [
ProductFactory.create({ price: 99.99 }),
ProductFactory.create({ price: 199.99 }),
];
// Create order with relationships
return {
id: faker.string.uuid(),
userId: user.id,
user,
items: products.map((product) => ({
id: faker.string.uuid(),
productId: product.id,
product,
quantity: 1,
price: product.price,
})),
total: products.reduce((sum, p) => sum + p.price, 0),
status: "PENDING",
createdAt: new Date(),
...overrides,
};
} }
Database Persistence
// factories/UserFactory.ts with persistence export class UserFactory { private prisma: PrismaClient;
constructor(prisma: PrismaClient) { this.prisma = prisma; }
async create(overrides?: Partial<User>): Promise<User> { const data = { email: faker.internet.email(), name: faker.person.fullName(), role: "USER", ...overrides, };
return this.prisma.user.create({ data });
}
async createMany(count: number): Promise<User[]> { return Promise.all(Array.from({ length: count }, () => this.create())); }
async createAdmin(): Promise<User> { return this.create({ role: "ADMIN" }); } }
// Usage in tests test("should list users", async () => { const userFactory = new UserFactory(prisma); await userFactory.createMany(5);
const users = await userService.list(); expect(users).toHaveLength(5); });
Traits Pattern
// factories/UserFactory.ts with traits export class UserFactory { private traits: string[] = [];
withTrait(trait: string): this { this.traits.push(trait); return this; }
build(): User { let user: User = { id: faker.string.uuid(), email: faker.internet.email(), name: faker.person.fullName(), role: "USER", createdAt: new Date(), };
// Apply traits
if (this.traits.includes("verified")) {
user.emailVerified = true;
user.verifiedAt = new Date();
}
if (this.traits.includes("suspended")) {
user.status = "SUSPENDED";
user.suspendedAt = new Date();
}
if (this.traits.includes("premium")) {
user.subscription = "PREMIUM";
user.subscriptionExpiresAt = faker.date.future();
}
return user;
} }
// Usage const verifiedUser = new UserFactory().withTrait("verified").build();
const suspendedPremiumUser = new UserFactory() .withTrait("suspended") .withTrait("premium") .build();
Sequence Generation
// factories/sequence.ts class Sequence { private counters = new Map<string, number>();
next(key: string): number { const current = this.counters.get(key) || 0; const next = current + 1; this.counters.set(key, next); return next; }
reset(key?: string): void { if (key) { this.counters.delete(key); } else { this.counters.clear(); } } }
const sequence = new Sequence();
// Usage in factory
export class UserFactory {
build(): User {
return {
id: faker.string.uuid(),
email: user${sequence.next("user")}@example.com,
name: Test User ${sequence.next("user")},
// ...
};
}
}
// Creates: user1@example.com, user2@example.com, etc.
Composable Factories
// factories/index.ts export const TestDataBuilder = { user: (overrides?: Partial<User>) => new UserFactory().with(overrides), product: (overrides?: Partial<Product>) => new ProductFactory().with(overrides), order: () => new OrderBuilder(),
// Composite builders checkoutScenario: async () => { const user = TestDataBuilder.user().build(); const products = [ TestDataBuilder.product({ price: 99.99 }).build(), TestDataBuilder.product({ price: 199.99 }).build(), ]; const order = await TestDataBuilder.order() .forUser(user) .withItem(products[0], 2) .withItem(products[1], 1) .build();
return { user, products, order };
}, };
// Usage test("should process checkout", async () => { const { user, order } = await TestDataBuilder.checkoutScenario();
const result = await checkoutService.process(order); expect(result.status).toBe("SUCCESS"); });
Realistic Data Generators
// generators/realistic.ts import { faker } from "@faker-js/faker";
export const RealisticData = { creditCard: () => ({ number: "4242424242424242", // Test card expiry: faker.date.future().toISOString().slice(0, 7), // YYYY-MM cvc: "123", name: faker.person.fullName(), }),
address: () => ({ street: faker.location.streetAddress(), city: faker.location.city(), state: faker.location.state(), zip: faker.location.zipCode(), country: "US", }),
product: () => ({ name: faker.commerce.productName(), description: faker.commerce.productDescription(), price: parseFloat(faker.commerce.price()), category: faker.commerce.department(), sku: faker.string.alphanumeric(10).toUpperCase(), }),
email: {
valid: () => faker.internet.email(),
invalid: () => "invalid-email",
disposable: () => ${faker.string.alphanumeric(8)}@tempmail.com,
},
};
Factory Registry
// factories/registry.ts class FactoryRegistry { private factories = new Map();
register<T>(name: string, factory: () => T): void { this.factories.set(name, factory); }
create<T>(name: string, overrides?: Partial<T>): T {
const factory = this.factories.get(name);
if (!factory) {
throw new Error(Factory not found: ${name});
}
const instance = factory();
return { ...instance, ...overrides };
}
}
const registry = new FactoryRegistry();
// Register factories registry.register("user", () => UserFactory.create()); registry.register("product", () => ProductFactory.create());
// Usage const user = registry.create("user", { role: "ADMIN" });
Test Helpers
// helpers/test-data.ts export async function seedTestDatabase(prisma: PrismaClient) { const userFactory = new UserFactory(prisma); const productFactory = new ProductFactory(prisma);
// Create base data const users = await userFactory.createMany(10); const products = await productFactory.createMany(20);
// Create relationships for (const user of users.slice(0, 5)) { await new OrderBuilder() .forUser(user) .withItem(products[0], 2) .withItem(products[1], 1) .asPaid() .build(); }
return { users, products }; }
// Usage beforeEach(async () => { await seedTestDatabase(prisma); });
Best Practices
-
Deterministic by default: Use seeded faker
-
Minimal data: Only create what's needed
-
Composable: Combine factories
-
Type-safe: Full TypeScript support
-
Relationships: Easy to create related data
-
Database-agnostic: Works with or without DB
-
Clear naming: Descriptive factory methods
Output Checklist
-
Factory classes created
-
Builder pattern implemented
-
Relationship handling
-
Database persistence option
-
Traits for variations
-
Sequence generation
-
Composable builders
-
Realistic data generators
-
Factory registry (optional)
-
Test helpers created