Files
gh-anton-abyzov-specweave-p…/agents/qa-engineer/templates/test-data-factory.ts
2025-11-29 17:57:09 +08:00

508 lines
12 KiB
TypeScript

/**
* Test Data Factory Template
*
* This template demonstrates best practices for creating reusable
* test data factories using the Factory pattern.
*
* Benefits:
* - Consistent test data generation
* - Easy to customize with overrides
* - Reduces test setup boilerplate
* - Type-safe with TypeScript
*/
import { faker } from '@faker-js/faker';
// ============================================================================
// TYPES
// ============================================================================
export interface User {
id: string;
email: string;
username: string;
firstName: string;
lastName: string;
role: 'admin' | 'user' | 'guest';
isActive: boolean;
createdAt: Date;
updatedAt: Date;
profile?: UserProfile;
}
export interface UserProfile {
bio: string;
avatar: string;
phoneNumber: string;
address: Address;
}
export interface Address {
street: string;
city: string;
state: string;
zipCode: string;
country: string;
}
export interface Product {
id: string;
name: string;
description: string;
price: number;
category: string;
inStock: boolean;
quantity: number;
imageUrl: string;
createdAt: Date;
}
export interface Order {
id: string;
userId: string;
items: OrderItem[];
subtotal: number;
tax: number;
total: number;
status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
shippingAddress: Address;
createdAt: Date;
updatedAt: Date;
}
export interface OrderItem {
productId: string;
quantity: number;
price: number;
}
// ============================================================================
// FACTORY FUNCTIONS
// ============================================================================
/**
* User Factory
*
* Creates realistic user test data with sensible defaults
*/
export class UserFactory {
/**
* Create a single user
*
* @param overrides - Partial user object to override defaults
* @returns Complete user object
*
* @example
* ```ts
* const admin = UserFactory.create({ role: 'admin' });
* const inactiveUser = UserFactory.create({ isActive: false });
* ```
*/
static create(overrides: Partial<User> = {}): User {
const firstName = faker.person.firstName();
const lastName = faker.person.lastName();
const email =
overrides.email || faker.internet.email({ firstName, lastName });
return {
id: faker.string.uuid(),
email,
username: faker.internet.userName({ firstName, lastName }),
firstName,
lastName,
role: 'user',
isActive: true,
createdAt: faker.date.past(),
updatedAt: faker.date.recent(),
...overrides,
};
}
/**
* Create multiple users
*
* @param count - Number of users to create
* @param overrides - Partial user object to override defaults for all users
* @returns Array of user objects
*
* @example
* ```ts
* const users = UserFactory.createMany(5);
* const admins = UserFactory.createMany(3, { role: 'admin' });
* ```
*/
static createMany(count: number, overrides: Partial<User> = {}): User[] {
return Array.from({ length: count }, () => this.create(overrides));
}
/**
* Create admin user
*
* @param overrides - Partial user object to override defaults
* @returns Admin user object
*/
static createAdmin(overrides: Partial<User> = {}): User {
return this.create({
role: 'admin',
...overrides,
});
}
/**
* Create user with complete profile
*
* @param overrides - Partial user object to override defaults
* @returns User with profile object
*/
static createWithProfile(overrides: Partial<User> = {}): User {
return this.create({
profile: {
bio: faker.person.bio(),
avatar: faker.image.avatar(),
phoneNumber: faker.phone.number(),
address: AddressFactory.create(),
},
...overrides,
});
}
/**
* Create inactive user
*
* @param overrides - Partial user object to override defaults
* @returns Inactive user object
*/
static createInactive(overrides: Partial<User> = {}): User {
return this.create({
isActive: false,
...overrides,
});
}
}
/**
* Address Factory
*
* Creates realistic address test data
*/
export class AddressFactory {
static create(overrides: Partial<Address> = {}): Address {
return {
street: faker.location.streetAddress(),
city: faker.location.city(),
state: faker.location.state(),
zipCode: faker.location.zipCode(),
country: faker.location.country(),
...overrides,
};
}
static createUS(overrides: Partial<Address> = {}): Address {
return this.create({
country: 'United States',
zipCode: faker.location.zipCode('#####'),
state: faker.location.state({ abbreviated: true }),
...overrides,
});
}
}
/**
* Product Factory
*
* Creates realistic product test data
*/
export class ProductFactory {
static create(overrides: Partial<Product> = {}): Product {
return {
id: faker.string.uuid(),
name: faker.commerce.productName(),
description: faker.commerce.productDescription(),
price: parseFloat(faker.commerce.price()),
category: faker.commerce.department(),
inStock: true,
quantity: faker.number.int({ min: 0, max: 100 }),
imageUrl: faker.image.url(),
createdAt: faker.date.past(),
...overrides,
};
}
static createMany(count: number, overrides: Partial<Product> = {}): Product[] {
return Array.from({ length: count }, () => this.create(overrides));
}
static createOutOfStock(overrides: Partial<Product> = {}): Product {
return this.create({
inStock: false,
quantity: 0,
...overrides,
});
}
static createExpensive(overrides: Partial<Product> = {}): Product {
return this.create({
price: faker.number.int({ min: 1000, max: 10000 }),
...overrides,
});
}
}
/**
* Order Factory
*
* Creates realistic order test data with items
*/
export class OrderFactory {
static create(overrides: Partial<Order> = {}): Order {
const items = overrides.items || [
{
productId: faker.string.uuid(),
quantity: faker.number.int({ min: 1, max: 5 }),
price: parseFloat(faker.commerce.price()),
},
];
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const tax = subtotal * 0.08; // 8% tax
const total = subtotal + tax;
return {
id: faker.string.uuid(),
userId: faker.string.uuid(),
items,
subtotal,
tax,
total,
status: 'pending',
shippingAddress: AddressFactory.createUS(),
createdAt: faker.date.recent(),
updatedAt: faker.date.recent(),
...overrides,
};
}
static createMany(count: number, overrides: Partial<Order> = {}): Order[] {
return Array.from({ length: count }, () => this.create(overrides));
}
static createWithItems(items: OrderItem[], overrides: Partial<Order> = {}): Order {
return this.create({ items, ...overrides });
}
static createShipped(overrides: Partial<Order> = {}): Order {
return this.create({
status: 'shipped',
...overrides,
});
}
static createCancelled(overrides: Partial<Order> = {}): Order {
return this.create({
status: 'cancelled',
...overrides,
});
}
}
// ============================================================================
// BUILDER PATTERN (Advanced)
// ============================================================================
/**
* User Builder
*
* Provides a fluent interface for building complex user objects
*
* @example
* ```ts
* const user = new UserBuilder()
* .withEmail('admin@example.com')
* .withRole('admin')
* .withProfile()
* .build();
* ```
*/
export class UserBuilder {
private user: Partial<User> = {};
withId(id: string): this {
this.user.id = id;
return this;
}
withEmail(email: string): this {
this.user.email = email;
return this;
}
withUsername(username: string): this {
this.user.username = username;
return this;
}
withName(firstName: string, lastName: string): this {
this.user.firstName = firstName;
this.user.lastName = lastName;
return this;
}
withRole(role: User['role']): this {
this.user.role = role;
return this;
}
withProfile(profile?: UserProfile): this {
this.user.profile = profile || {
bio: faker.person.bio(),
avatar: faker.image.avatar(),
phoneNumber: faker.phone.number(),
address: AddressFactory.create(),
};
return this;
}
inactive(): this {
this.user.isActive = false;
return this;
}
active(): this {
this.user.isActive = true;
return this;
}
build(): User {
return UserFactory.create(this.user);
}
}
// ============================================================================
// USAGE EXAMPLES
// ============================================================================
/**
* Example: Simple user creation
*/
export function exampleSimpleUser() {
const user = UserFactory.create();
const admin = UserFactory.createAdmin();
const users = UserFactory.createMany(5);
return { user, admin, users };
}
/**
* Example: Customized user creation
*/
export function exampleCustomUser() {
const user = UserFactory.create({
email: 'custom@example.com',
role: 'admin',
isActive: false,
});
return user;
}
/**
* Example: Builder pattern
*/
export function exampleBuilder() {
const user = new UserBuilder()
.withEmail('builder@example.com')
.withName('John', 'Doe')
.withRole('admin')
.withProfile()
.active()
.build();
return user;
}
/**
* Example: Order with products
*/
export function exampleOrder() {
// Create products
const products = ProductFactory.createMany(3);
// Create order items from products
const items: OrderItem[] = products.map((product) => ({
productId: product.id,
quantity: faker.number.int({ min: 1, max: 3 }),
price: product.price,
}));
// Create order with items
const order = OrderFactory.createWithItems(items);
return { products, order };
}
// ============================================================================
// TEST USAGE
// ============================================================================
/**
* Example test using factories
*/
import { describe, it, expect } from 'vitest';
describe('UserService (using factories)', () => {
it('should create user', () => {
// ARRANGE
const userData = UserFactory.create();
// ACT
const result = userService.create(userData);
// ASSERT
expect(result.id).toBeDefined();
expect(result.email).toBe(userData.email);
});
it('should only allow admins to delete users', () => {
// ARRANGE
const admin = UserFactory.createAdmin();
const regularUser = UserFactory.create();
// ACT & ASSERT
expect(() => userService.deleteUser(regularUser.id, admin)).not.toThrow();
expect(() => userService.deleteUser(admin.id, regularUser)).toThrow('Unauthorized');
});
it('should calculate order total correctly', () => {
// ARRANGE
const order = OrderFactory.create({
items: [
{ productId: '1', quantity: 2, price: 50 },
{ productId: '2', quantity: 1, price: 30 },
],
});
// ACT
const total = orderService.calculateTotal(order);
// ASSERT
expect(total).toBe(140.4); // (50*2 + 30*1) * 1.08 tax
});
});
// ============================================================================
// BEST PRACTICES
// ============================================================================
/*
✅ Use realistic data (faker.js)
✅ Provide sensible defaults
✅ Allow overrides for customization
✅ Type-safe with TypeScript
✅ Create helper methods (createAdmin, createInactive, etc.)
✅ Builder pattern for complex objects
✅ Consistent naming (create, createMany, createWith...)
✅ Document with JSDoc
✅ Export all factories for reuse
✅ Keep factories simple and focused
*/