interface-forge
Version:
A TypeScript library for creating strongly typed mock data factories using Faker.js for test data generation
371 lines (330 loc) • 10.3 kB
text/typescript
/**
* Example 06: Hooks and Validation
*
* This example demonstrates how to use beforeBuild and afterBuild hooks
* to add validation, data transformation, and business logic to your factories.
*
* Topics covered:
* - Using synchronous hooks with build()
* - Using asynchronous hooks with buildAsync()
* - Data validation and transformation
* - Conditional logic in hooks
* - Error handling with hooks
*/
import { Factory } from 'interface-forge';
interface User {
createdAt: Date;
email: string;
id: string;
isActive: boolean;
role: 'admin' | 'guest' | 'user';
username: string;
}
const UserFactory = new Factory<User>((factory) => ({
createdAt: factory.date.recent(),
email: '',
id: factory.string.uuid(),
isActive: true,
role: 'user',
username: '',
}))
.beforeBuild((params) => {
if (!params.email && params.username) {
params.email = `${params.username}@example.com`;
} else if (!params.username && params.email) {
[params.username] = params.email.split('@');
} else if (!params.email && !params.username) {
const randomUsername = `user_${Date.now()}`;
params.email = `${randomUsername}@example.com`;
params.username = randomUsername;
}
return params;
})
.afterBuild((user) => {
user.email = user.email.toLowerCase();
user.username = user.username.toLowerCase();
return user;
});
const user1 = UserFactory.build({ username: 'JohnDoe' });
console.log('User 1:', user1);
const user2 = UserFactory.build({ email: 'Jane.Smith@company.com' });
console.log('User 2:', user2);
interface Product {
category: string;
id: string;
isApproved: boolean;
name: string;
price: number;
sku: string;
}
async function checkPriceRange(
category: string,
price: number,
): Promise<boolean> {
await new Promise((resolve) => setTimeout(resolve, 10));
const priceRanges: Record<string, [number, number]> = {
books: [5, 100],
clothing: [10, 500],
electronics: [50, 5000],
};
const range = priceRanges[category] ?? [0, 10_000];
return price >= range[0] && price <= range[1];
}
async function validateSKU(sku: string): Promise<boolean> {
await new Promise((resolve) => setTimeout(resolve, 10));
return /^[A-Z]{3}-\d{4}$/.test(sku);
}
const ProductFactory = new Factory<Product>((factory) => ({
category: factory.helpers.arrayElement([
'electronics',
'books',
'clothing',
]),
id: factory.string.uuid(),
isApproved: false,
name: factory.commerce.productName(),
price: factory.number.float({ fractionDigits: 2, max: 1000, min: 10 }),
sku: '',
}))
.beforeBuild((params) => {
if (!params.sku) {
const category = params.category ?? 'GEN';
const code = Math.floor(Math.random() * 10_000)
.toString()
.padStart(4, '0');
params.sku = `${category.slice(0, 3).toUpperCase()}-${code}`;
}
return params;
})
.afterBuild(async (product) => {
const isValidSKU = await validateSKU(product.sku);
if (!isValidSKU) {
throw new Error(`Invalid SKU format: ${product.sku}`);
}
const isPriceValid = await checkPriceRange(
product.category,
product.price,
);
if (!isPriceValid) {
throw new Error(
`Price ${product.price} is out of range for category ${product.category}`,
);
}
product.isApproved = true;
return product;
});
try {
ProductFactory.build();
} catch (error) {
console.log(
'Expected error:',
error instanceof Error ? error.message : String(error),
);
}
const createProducts = async () => {
const product = await ProductFactory.buildAsync();
console.log('Validated product:', product);
try {
await ProductFactory.buildAsync({
category: 'books',
price: 10_000,
sku: 'INVALID-SKU',
});
} catch (error) {
console.log(
'Validation error:',
error instanceof Error ? error.message : String(error),
);
}
};
interface BlogPost {
author: string;
content: string;
id: string;
publishedAt?: Date;
slug: string;
status: 'archived' | 'draft' | 'published';
tags: string[];
title: string;
}
const BlogPostFactory = new Factory<BlogPost>((factory) => ({
author: factory.person.fullName(),
content: factory.lorem.paragraphs(3),
id: factory.string.uuid(),
slug: '',
status: 'draft',
tags: [],
title: factory.lorem.sentence(),
}))
.beforeBuild((params) => {
if (!params.slug && params.title) {
params.slug = params.title
.toLowerCase()
.replaceAll(/[^a-z0-9]+/g, '-')
.replaceAll(/^-|-$/g, '');
}
if (params.tags?.length === 0 && params.content) {
const contentLower = params.content.toLowerCase();
const autoTags: string[] = [];
if (
contentLower.includes('javascript') ||
contentLower.includes('typescript')
) {
autoTags.push('programming');
}
if (
contentLower.includes('react') ||
contentLower.includes('vue')
) {
autoTags.push('frontend');
}
if (
contentLower.includes('node') ||
contentLower.includes('express')
) {
autoTags.push('backend');
}
params.tags = autoTags;
}
return params;
})
.afterBuild((post) => {
if (post.status === 'published' && !post.publishedAt) {
post.publishedAt = new Date();
}
if (post.status !== 'published' && post.publishedAt) {
delete post.publishedAt;
}
return post;
});
const draftPost = BlogPostFactory.build({
content:
'TypeScript is a powerful language that adds static typing to JavaScript...',
title: 'Getting Started with TypeScript',
});
console.log('Draft post:', {
publishedAt: draftPost.publishedAt,
slug: draftPost.slug,
tags: draftPost.tags,
title: draftPost.title,
});
const publishedPost = BlogPostFactory.build({
status: 'published',
title: 'Building APIs with Node.js and Express',
});
console.log('Published post:', {
publishedAt: publishedPost.publishedAt,
status: publishedPost.status,
});
interface Order {
customerId: string;
id: string;
items: { price: number; productId: string; quantity: number }[];
shipping: number;
status: 'delivered' | 'pending' | 'processing' | 'shipped';
subtotal: number;
tax: number;
total: number;
}
const OrderFactory = new Factory<Order>((factory) => ({
customerId: factory.string.uuid(),
id: factory.string.uuid(),
items: [],
shipping: 0,
status: 'pending',
subtotal: 0,
tax: 0,
total: 0,
}))
.beforeBuild((params) => {
if (!params.items || params.items.length === 0) {
const itemCount = Math.floor(Math.random() * 5) + 1;
params.items = Array.from({ length: itemCount }, () => ({
price: Number.parseFloat((Math.random() * 100 + 10).toFixed(2)),
productId: crypto.randomUUID(),
quantity: Math.floor(Math.random() * 5) + 1,
}));
}
return params;
})
.afterBuild((order) => {
order.subtotal = order.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
);
order.tax = Number.parseFloat((order.subtotal * 0.1).toFixed(2));
order.shipping = order.subtotal >= 100 ? 0 : 10;
order.total = Number.parseFloat(
(order.subtotal + order.tax + order.shipping).toFixed(2),
);
if (order.total < 10) {
throw new Error('Order total must be at least $10');
}
return order;
});
const order = OrderFactory.build();
console.log('Order summary:', {
items: order.items.length,
shipping: order.shipping,
subtotal: order.subtotal,
tax: order.tax,
total: order.total,
});
interface Address {
city: string;
country: string;
state: string;
street: string;
zipCode: string;
}
const AddressFactory = new Factory<Address>((factory) => ({
city: factory.location.city(),
country: 'USA',
state: factory.location.state({ abbreviated: true }),
street: factory.location.streetAddress(),
zipCode: '',
})).afterBuild((address) => {
const stateZipRanges: Record<string, [number, number]> = {
CA: [90_000, 96_199],
NY: [10_000, 14_999],
TX: [73_301, 88_900],
};
if (!address.zipCode) {
const range = stateZipRanges[address.state] ?? [10_000, 99_999];
const zip = Math.floor(
Math.random() * (range[1] - range[0]) + range[0],
);
address.zipCode = zip.toString().padStart(5, '0');
}
return address;
});
interface Customer {
billingAddress: Address;
email: string;
id: string;
name: string;
sameAsBilling: boolean;
shippingAddress: Address;
}
const CustomerFactory = new Factory<Customer>((factory) => ({
billingAddress: AddressFactory.build(),
email: factory.internet.email(),
id: factory.string.uuid(),
name: factory.person.fullName(),
sameAsBilling: factory.datatype.boolean(),
shippingAddress: AddressFactory.build(),
})).afterBuild((customer) => {
if (customer.sameAsBilling) {
customer.shippingAddress = { ...customer.billingAddress };
}
return customer;
});
const customer = CustomerFactory.build({ sameAsBilling: true });
console.log('Customer addresses:', {
areSame:
JSON.stringify(customer.billingAddress) ===
JSON.stringify(customer.shippingAddress),
billing: customer.billingAddress,
shipping: customer.shippingAddress,
});
void createProducts().catch(console.error);