# ADR-045: Test Data Factories and Fixtures **Date**: 2026-01-09 **Status**: Accepted **Implemented**: 2026-01-09 ## Context The application has a complex domain model with many entity types: - Users, Profiles, Addresses - Flyers, FlyerItems, Stores - ShoppingLists, ShoppingListItems - Recipes, RecipeIngredients - Gamification (points, badges, leaderboards) - And more... Testing requires realistic mock data that: 1. Satisfies TypeScript types. 2. Has valid relationships between entities. 3. Is customizable for specific test scenarios. 4. Is consistent across test suites. 5. Avoids boilerplate in test files. ## Decision We will implement a **factory function pattern** for test data generation: 1. **Centralized Mock Factories**: All factories in a single, organized file. 2. **Sensible Defaults**: Each factory produces valid data with minimal input. 3. **Override Support**: Factories accept partial overrides for customization. 4. **Relationship Helpers**: Factories can generate related entities. 5. **Type Safety**: Factories return properly typed objects. ### Design Principles - **Convention over Configuration**: Factories work with zero arguments. - **Composability**: Factories can call other factories. - **Immutability**: Each call returns a new object (no shared references). - **Predictability**: Deterministic output when seeded. ## Implementation Details ### Factory File Structure Located in `src/test/mockFactories.ts`: ```typescript import { v4 as uuidv4 } from 'uuid'; import type { User, UserProfile, Flyer, FlyerItem, ShoppingList, // ... other types } from '../types'; // ============================================ // PRIMITIVE HELPERS // ============================================ let idCounter = 1; export const nextId = () => idCounter++; export const resetIdCounter = () => { idCounter = 1; }; export const randomEmail = () => `user-${uuidv4().slice(0, 8)}@test.com`; export const randomDate = (daysAgo = 0) => { const date = new Date(); date.setDate(date.getDate() - daysAgo); return date.toISOString(); }; // ============================================ // USER FACTORIES // ============================================ export const createMockUser = (overrides: Partial = {}): User => ({ user_id: nextId(), email: randomEmail(), name: 'Test User', role: 'user', created_at: randomDate(30), updated_at: randomDate(), ...overrides, }); export const createMockUserProfile = (overrides: Partial = {}): UserProfile => { const user = createMockUser(overrides.user); return { user, profile: createMockProfile({ user_id: user.user_id, ...overrides.profile }), address: overrides.address ?? null, preferences: overrides.preferences ?? null, }; }; // ============================================ // FLYER FACTORIES // ============================================ export const createMockFlyer = (overrides: Partial = {}): Flyer => ({ flyer_id: nextId(), file_name: 'test-flyer.jpg', image_url: 'https://example.com/flyer.jpg', icon_url: 'https://example.com/flyer-icon.jpg', checksum: uuidv4(), store_name: 'Test Store', store_address: '123 Test St', valid_from: randomDate(7), valid_to: randomDate(-7), // 7 days in future item_count: 10, status: 'approved', uploaded_by: null, created_at: randomDate(7), updated_at: randomDate(), ...overrides, }); export const createMockFlyerItem = (overrides: Partial = {}): FlyerItem => ({ flyer_item_id: nextId(), flyer_id: overrides.flyer_id ?? nextId(), item: 'Test Product', price_display: '$2.99', price_in_cents: 299, quantity: 'each', category_name: 'Groceries', master_item_id: null, view_count: 0, click_count: 0, created_at: randomDate(7), updated_at: randomDate(), ...overrides, }); // ============================================ // FLYER WITH ITEMS (COMPOSITE) // ============================================ export const createMockFlyerWithItems = ( flyerOverrides: Partial = {}, itemCount = 5, ): { flyer: Flyer; items: FlyerItem[] } => { const flyer = createMockFlyer(flyerOverrides); const items = Array.from({ length: itemCount }, (_, i) => createMockFlyerItem({ flyer_id: flyer.flyer_id, item: `Product ${i + 1}`, price_in_cents: 100 + i * 50, }), ); flyer.item_count = items.length; return { flyer, items }; }; // ============================================ // SHOPPING LIST FACTORIES // ============================================ export const createMockShoppingList = (overrides: Partial = {}): ShoppingList => ({ shopping_list_id: nextId(), user_id: overrides.user_id ?? nextId(), name: 'Weekly Groceries', is_active: true, created_at: randomDate(14), updated_at: randomDate(), ...overrides, }); export const createMockShoppingListItem = ( overrides: Partial = {}, ): ShoppingListItem => ({ shopping_list_item_id: nextId(), shopping_list_id: overrides.shopping_list_id ?? nextId(), item_name: 'Milk', quantity: 1, is_purchased: false, created_at: randomDate(7), updated_at: randomDate(), ...overrides, }); ``` ### Usage in Tests ```typescript import { createMockUser, createMockFlyer, createMockFlyerWithItems, resetIdCounter, } from '../test/mockFactories'; describe('FlyerService', () => { beforeEach(() => { resetIdCounter(); // Consistent IDs across tests }); it('should get flyer by ID', async () => { const mockFlyer = createMockFlyer({ store_name: 'Walmart' }); mockDb.query.mockResolvedValue({ rows: [mockFlyer] }); const result = await flyerService.getFlyerById(mockFlyer.flyer_id); expect(result.store_name).toBe('Walmart'); }); it('should return flyer with items', async () => { const { flyer, items } = createMockFlyerWithItems( { store_name: 'Costco' }, 10, // 10 items ); mockDb.query.mockResolvedValueOnce({ rows: [flyer] }).mockResolvedValueOnce({ rows: items }); const result = await flyerService.getFlyerWithItems(flyer.flyer_id); expect(result.flyer.store_name).toBe('Costco'); expect(result.items).toHaveLength(10); }); }); ``` ### Bulk Data Generation For integration tests or seeding: ```typescript export const createMockDataset = () => { const users = Array.from({ length: 10 }, () => createMockUser()); const flyers = Array.from({ length: 5 }, () => createMockFlyer()); const flyersWithItems = flyers.map((flyer) => ({ flyer, items: Array.from({ length: Math.floor(Math.random() * 20) + 5 }, () => createMockFlyerItem({ flyer_id: flyer.flyer_id }), ), })); return { users, flyers, flyersWithItems }; }; ``` ### API Response Factories For testing API handlers: ```typescript export const createMockApiResponse = ( data: T, overrides: Partial> = {}, ): ApiResponse => ({ success: true, data, meta: { timestamp: new Date().toISOString(), requestId: uuidv4(), ...overrides.meta, }, ...overrides, }); export const createMockPaginatedResponse = ( items: T[], page = 1, pageSize = 20, ): PaginatedApiResponse => ({ success: true, data: items, meta: { timestamp: new Date().toISOString(), requestId: uuidv4(), }, pagination: { page, pageSize, totalItems: items.length, totalPages: Math.ceil(items.length / pageSize), hasMore: false, }, }); ``` ### Database Query Mock Helpers ```typescript export const mockQueryResult = (rows: T[]) => ({ rows, rowCount: rows.length, }); export const mockEmptyResult = () => ({ rows: [], rowCount: 0, }); export const mockInsertResult = (inserted: T) => ({ rows: [inserted], rowCount: 1, }); ``` ## Test Cleanup Utilities ```typescript // For integration tests with real database export const cleanupTestData = async (pool: Pool) => { await pool.query('DELETE FROM flyer_items WHERE flyer_id > 1000000'); await pool.query('DELETE FROM flyers WHERE flyer_id > 1000000'); await pool.query('DELETE FROM users WHERE user_id > 1000000'); }; // Mark test data with high IDs export const createTestFlyer = (overrides: Partial = {}) => createMockFlyer({ flyer_id: 1000000 + nextId(), ...overrides }); ``` ## Consequences ### Positive - **Consistency**: All tests use the same factory patterns. - **Type Safety**: Factories return correctly typed objects. - **Reduced Boilerplate**: Tests focus on behavior, not data setup. - **Maintainability**: Update factory once, all tests benefit. - **Flexibility**: Easy to create edge case data. ### Negative - **Single Large File**: Factory file can become large. - **Learning Curve**: New developers must learn factory patterns. - **Maintenance**: Factories must be updated when types change. ### Mitigation - Split factories into multiple files if needed (by domain). - Add JSDoc comments explaining each factory. - Use TypeScript to catch type mismatches automatically. ## Key Files - `src/test/mockFactories.ts` - All mock factory functions - `src/test/testUtils.ts` - Test helper utilities - `src/test/setup.ts` - Global test setup with factory reset ## Related ADRs - [ADR-010](./0010-testing-strategy-and-standards.md) - Testing Strategy - [ADR-040](./0040-testing-economics-and-priorities.md) - Testing Economics - [ADR-027](./0027-standardized-naming-convention-for-ai-and-database-types.md) - Type Naming