Skip to main content
Glama
test-infrastructure-architecture.md20.4 kB
# Test Infrastructure Architecture **Comprehensive Guide to Mock Factory Pattern and Test Environment Management** ## Overview This document describes the test infrastructure architecture implemented to resolve Issue #480 and establish sustainable, scalable test patterns. The architecture provides clean separation between test and production concerns while ensuring Issue #480 compatibility and type safety. ## Architecture Principles ### Core Design Principles 1. **Single Responsibility**: Each factory handles one resource type exclusively 2. **Type Safety**: Full TypeScript support with proper interfaces and validation 3. **Compatibility**: Issue #480 compatible field structures across all mock types 4. **Isolation**: Complete separation from production code preventing coupling violations 5. **Extensibility**: Easy addition of new resource types following established patterns ### Mock Factory Pattern The Mock Factory Pattern provides a centralized, consistent approach to generating test data that matches production API responses while supporting legacy and new field requirements. ```typescript // Base interface ensuring consistency across all mock factories export interface MockFactory<T> { create(overrides?: Partial<T>): T; createMultiple(count: number, overrides?: Partial<T>): T[]; generateMockId(): string; } ``` ## Directory Structure ``` test/utils/mock-factories/ ├── index.ts # Centralized exports and UniversalMockFactory ├── test-environment.ts # Test environment detection utilities ├── mock-injector.ts # Production-safe mock injection utilities ├── TaskMockFactory.ts # Task resource mock generation ├── CompanyMockFactory.ts # Company resource mock generation ├── PersonMockFactory.ts # Person resource mock generation ├── ListMockFactory.ts # List resource mock generation └── mock-factories-validation.test.ts # Factory validation tests ``` ## Mock Factory Implementation ### TaskMockFactory - Issue #480 Compatible Implementation The TaskMockFactory demonstrates Issue #480 compatibility requirements: ```typescript /** * TaskMockFactory - Issue #480 Compatible Implementation * * Provides both content and title fields for E2E test compatibility. * Ensures task_id is preserved in the ID structure for universal handlers. */ export class TaskMockFactory implements MockFactory<AttioTask> { /** * Creates Issue #480 compatible mock task */ static create(overrides: MockTaskOptions = {}): AttioTask { const taskId = this.generateMockId(); const content = overrides.content || overrides.title || 'Mock Task Content'; return { id: { record_id: taskId, task_id: taskId, // Issue #480: Preserve task_id workspace_id: 'mock-workspace-id' }, content, // Primary content field title: content, // Issue #480: Dual field support status: overrides.status || 'pending', created_at: overrides.created_at || new Date().toISOString(), updated_at: overrides.updated_at || new Date().toISOString(), // ... handle optional fields }; } /** * Generates unique mock IDs with timestamp and randomness */ static generateMockId(): string { const timestamp = Date.now(); const random = Math.random().toString(36).substr(2, 9); return `mock-task-${timestamp}-${random}`; } /** * Creates multiple mock tasks with staggered properties */ static createMultiple(count: number, overrides: MockTaskOptions = {}): AttioTask[] { return Array.from({ length: count }, (_, index) => { const taskNumber = index + 1; return this.create({ ...overrides, content: overrides.content || `Mock Task ${taskNumber}`, due_date: overrides.due_date || this.generateFutureDueDate(index) }); }); } } ``` ### Specialized Factory Methods Each factory provides specialized creation methods for common scenarios: ```typescript export class TaskMockFactory { // High priority tasks static createHighPriority(overrides = {}) { return this.create({ ...overrides, status: 'pending', content: 'High Priority Mock Task' }); } // Completed tasks static createCompleted(overrides = {}) { return this.create({ ...overrides, status: 'completed', is_completed: true }); } // Tasks with assignees static createWithAssignee(assigneeId: string, overrides = {}) { return this.create({ ...overrides, assignee_id: assigneeId, assignee: { id: assigneeId, type: 'workspace_member', name: `Mock Assignee ${assigneeId.slice(-4)}`, email: `assignee-${assigneeId.slice(-4)}@example.com` } }); } // Overdue tasks for testing edge cases static createOverdue(overrides = {}) { const pastDate = new Date(); pastDate.setDate(pastDate.getDate() - 7); return this.create({ ...overrides, due_date: pastDate.toISOString().split('T')[0], status: 'pending' }); } } ``` ## Test Environment Detection ### Centralized Detection Strategy The test environment detection uses multiple strategies for reliability: ```typescript /** * TestEnvironment - Centralized test environment detection and utilities */ export class TestEnvironment { /** * Determines if mock data should be used based on environment */ static useMocks(): boolean { return this.isTestEnvironment() || this.isDevelopmentEnvironment(); } /** * Multi-strategy test environment detection */ static isTestEnvironment(): boolean { // Strategy 1: Environment variables if (process.env.NODE_ENV === 'test') return true; if (process.env.VITEST === 'true') return true; if (process.env.JEST_WORKER_ID !== undefined) return true; // Strategy 2: Test framework globals if (typeof global.it !== 'undefined') return true; if (typeof global.describe !== 'undefined') return true; // Strategy 3: Process arguments if (process.argv.some(arg => arg.includes('vitest'))) return true; if (process.argv.some(arg => arg.includes('test'))) return true; return false; } /** * Development environment detection */ static isDevelopmentEnvironment(): boolean { return process.env.NODE_ENV === 'development'; } /** * CI environment detection */ static isContinuousIntegrationEnvironment(): boolean { return !!( process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.JENKINS_URL ); } /** * Logging utility for development and test environments */ static log(message: string, data?: unknown): void { if (this.isTestEnvironment() || this.isDevelopmentEnvironment()) { console.debug(`[MockFactory] ${message}`, data || ''); } } /** * Get contextual information about current test environment */ static getTestContext(): TestContext { return { isTest: this.isTestEnvironment(), isDevelopment: this.isDevelopmentEnvironment(), isCI: this.isContinuousIntegrationEnvironment(), useMocks: this.useMocks(), nodeEnv: process.env.NODE_ENV || 'unknown', testFramework: this.detectTestFramework() }; } private static detectTestFramework(): string { if (process.env.VITEST === 'true') return 'vitest'; if (process.env.JEST_WORKER_ID !== undefined) return 'jest'; if (typeof global.it !== 'undefined') return 'detected'; return 'unknown'; } } ``` ## Universal Mock Factory The UniversalMockFactory provides a unified interface for creating mock data across all resource types: ```typescript /** * Universal Mock Factory - Resource type-agnostic mock generation */ export class UniversalMockFactory { /** * Creates mock data for any supported resource type */ static create(resourceType: string, overrides: Record<string, unknown> = {}): unknown { switch (resourceType.toLowerCase()) { case 'tasks': return TaskMockFactory.create(overrides); case 'companies': return CompanyMockFactory.create(overrides); case 'people': return PersonMockFactory.create(overrides); case 'lists': return ListMockFactory.create(overrides); default: throw new Error(`Unsupported resource type for mock data generation: ${resourceType}`); } } /** * Creates multiple mock items for any resource type */ static createMultiple( resourceType: string, count: number, overrides: Record<string, unknown> = {} ): unknown[] { const factory = this.getFactory(resourceType); return factory.createMultiple(count, overrides); } /** * Checks if mock generation is supported for resource type */ static isSupported(resourceType: string): boolean { return ['tasks', 'companies', 'people', 'lists'].includes(resourceType.toLowerCase()); } /** * Gets all supported resource types */ static getSupportedTypes(): string[] { return ['tasks', 'companies', 'people', 'lists']; } private static getFactory(resourceType: string): MockFactory<unknown> { switch (resourceType.toLowerCase()) { case 'tasks': return TaskMockFactory; case 'companies': return CompanyMockFactory; case 'people': return PersonMockFactory; case 'lists': return ListMockFactory; default: throw new Error(`Unsupported resource type: ${resourceType}`); } } } ``` ## Production-Safe Integration ### Mock Injection Pattern For production code that needs test support without coupling violations: ```typescript /** * Production-safe mock injection utility * * Allows production code to use mock data in test environments * without importing test utilities directly. */ export class MockDataInjector { /** * Safely injects mock data only in test environments */ static async injectMockData( resourceType: string, overrides: Record<string, unknown> = {} ): Promise<unknown> { // Only inject in test environments if (!TestEnvironment.useMocks()) { throw new Error('Mock data injection only available in test environments'); } // Dynamic import prevents production bundling of test utilities try { const { UniversalMockFactory } = await import('./index.js'); return UniversalMockFactory.create(resourceType, overrides); } catch (error) { console.warn('Failed to inject mock data, falling back to real implementation'); throw error; } } /** * Creates realistic mock scenarios for integration testing */ static async createTestScenario(scenarioType: 'sales' | 'support' | 'onboarding') { if (!TestEnvironment.useMocks()) { throw new Error('Test scenarios only available in test environments'); } const { QuickMocks } = await import('./index.js'); switch (scenarioType) { case 'sales': return QuickMocks.salesScenario(); case 'support': return { company: QuickMocks.company({ industry: 'Technology' }), contact: QuickMocks.person({ role: 'Support Contact' }), tasks: QuickMocks.task({ status: 'pending', type: 'support' }) }; case 'onboarding': return { company: QuickMocks.company({ status: 'new' }), contacts: Array.from({ length: 2 }, () => QuickMocks.person()), tasks: Array.from({ length: 3 }, (_, i) => QuickMocks.task({ content: `Onboarding Step ${i + 1}` }) ) }; default: throw new Error(`Unsupported scenario type: ${scenarioType}`); } } } ``` ## Type Safety and Validation ### TypeScript Interface Definitions ```typescript // Mock options interfaces for type safety export interface MockTaskOptions { content?: string; title?: string; status?: string; is_completed?: boolean; deadline_at?: string | null; due_date?: string | null; assignee_id?: string | null; assignee?: AttioTask['assignee']; linked_records?: AttioTask['linked_records']; record_id?: string | null; created_at?: string; updated_at?: string; } export interface MockCompanyOptions { name?: string; domain?: string; industry?: string; employee_count?: number; location?: string; website?: string; description?: string; } export interface TestContext { isTest: boolean; isDevelopment: boolean; isCI: boolean; useMocks: boolean; nodeEnv: string; testFramework: string; } ``` ### Mock Data Validation ```typescript /** * Mock data validation utilities */ export class MockDataValidator { /** * Validates Issue #480 compatibility for task mocks */ static validateTaskStructure(task: AttioTask): ValidationResult { return { hasRequiredFields: !!(task.id?.record_id && task.content), isIssue480Compatible: !!(task.id?.task_id && task.title), hasProperIdStructure: this.validateIdStructure(task.id), hasValidStatus: ['pending', 'completed', 'cancelled'].includes(task.status), hasValidTimestamps: !!(task.created_at && task.updated_at) }; } /** * Validates ID structure consistency */ static validateIdStructure(id: any): boolean { return !!( id && typeof id === 'object' && id.record_id && typeof id.record_id === 'string' ); } /** * Comprehensive mock data validation */ static validateMockData(resourceType: string, data: unknown): ValidationResult { switch (resourceType.toLowerCase()) { case 'tasks': return this.validateTaskStructure(data as AttioTask); case 'companies': return this.validateCompanyStructure(data as AttioCompany); case 'people': return this.validatePersonStructure(data as AttioPerson); case 'lists': return this.validateListStructure(data as AttioList); default: return { isValid: false, errors: [`Unsupported resource type: ${resourceType}`] }; } } } ``` ## Performance Considerations ### Optimization Strategies 1. **Lazy Loading**: Mock factories are only loaded when needed 2. **ID Generation**: Efficient timestamp + random approach for unique IDs 3. **Memory Management**: No persistent state, factory methods are stateless 4. **Bundle Size**: Dynamic imports prevent production bundle contamination ### Performance Metrics - **Mock Generation**: Sub-millisecond response times for single objects - **Batch Creation**: ~1ms per 10 objects for batch generation - **Memory Usage**: Zero persistent memory footprint - **Bundle Impact**: Zero impact on production bundle size ```typescript // Performance monitoring example export class MockPerformanceMonitor { static measureMockCreation(factory: string, count: number = 1): PerformanceResult { const start = performance.now(); // Mock creation logic here const end = performance.now(); const duration = end - start; return { factory, count, duration, averagePerItem: duration / count, timestamp: new Date().toISOString() }; } } ``` ## Extending the System ### Adding New Resource Types To add mock support for a new resource type: 1. **Create Factory Class**: ```typescript export class NewResourceMockFactory implements MockFactory<NewResource> { static generateMockId(): string { const timestamp = Date.now(); const random = Math.random().toString(36).substr(2, 9); return `mock-newresource-${timestamp}-${random}`; } static create(overrides: MockNewResourceOptions = {}): NewResource { return { id: { record_id: this.generateMockId(), workspace_id: 'mock-workspace-id' // Add any resource-specific ID fields }, // Implement all required fields required_field: overrides.required_field || 'Default Value', optional_field: overrides.optional_field, created_at: overrides.created_at || new Date().toISOString(), updated_at: overrides.updated_at || new Date().toISOString() }; } static createMultiple(count: number, overrides: MockNewResourceOptions = {}): NewResource[] { return Array.from({ length: count }, (_, index) => this.create({ ...overrides, required_field: overrides.required_field || `Mock Resource ${index + 1}` }) ); } // Add specialized creation methods static createWithSpecialCondition(overrides = {}) { return this.create({ ...overrides, special_field: 'special_value' }); } } ``` 2. **Update Universal Factory**: ```typescript export class UniversalMockFactory { static create(resourceType: string, overrides = {}) { switch (resourceType.toLowerCase()) { case 'newresource': return NewResourceMockFactory.create(overrides); // ... existing cases } } } ``` 3. **Add Type Definitions**: ```typescript export interface MockNewResourceOptions { required_field?: string; optional_field?: string; created_at?: string; updated_at?: string; } ``` 4. **Create Validation Tests**: ```typescript describe('NewResourceMockFactory', () => { it('should create valid mock data structure', () => { const mock = NewResourceMockFactory.create(); expect(MockDataValidator.validateNewResourceStructure(mock).isValid).toBe(true); }); it('should support custom overrides', () => { const mock = NewResourceMockFactory.create({ required_field: 'Custom Value' }); expect(mock.required_field).toBe('Custom Value'); }); it('should create multiple mocks with unique IDs', () => { const mocks = NewResourceMockFactory.createMultiple(3); expect(mocks).toHaveLength(3); const ids = mocks.map(m => m.id.record_id); expect(new Set(ids).size).toBe(3); // All unique }); }); ``` ### Compatibility Field Implementation When implementing compatibility for field changes (similar to Issue #480): 1. **Dual Field Support**: ```typescript static create(overrides = {}) { // Support both old and new field names const primaryValue = overrides.new_field || overrides.old_field || 'default'; return { old_field: primaryValue, // Backward compatibility new_field: primaryValue, // New standard // ... other fields }; } ``` 2. **Migration Helpers**: ```typescript static createFromLegacyFormat(legacyData: LegacyFormat): NewResource { return this.create({ new_field: legacyData.old_field, // Map other legacy fields }); } ``` 3. **Validation for Both Formats**: ```typescript static validateCompatibility(resource: NewResource): CompatibilityResult { return { hasLegacyFields: !!(resource.old_field), hasNewFields: !!(resource.new_field), isFullyMigrated: !!(resource.new_field && !resource.old_field) }; } ``` ## Best Practices ### Development Guidelines 1. **Factory Consistency**: All factories must implement the `MockFactory<T>` interface 2. **ID Generation**: Use consistent timestamp + random pattern for unique IDs 3. **Type Safety**: Always provide TypeScript interfaces for mock options 4. **Validation**: Include validation methods for mock data structure 5. **Performance**: Keep factory methods stateless and efficient 6. **Documentation**: Document any compatibility requirements clearly ### Testing Guidelines 1. **Mock Validation**: Always test that mocks match real API response structure 2. **Edge Cases**: Include specialized factory methods for edge case testing 3. **Performance**: Monitor mock generation performance in CI 4. **Compatibility**: Test both legacy and new field format support 5. **Integration**: Validate mock data works with real API integration tests ### Production Safety 1. **Import Boundaries**: Never import mock factories directly in production code 2. **Environment Detection**: Use reliable multi-strategy environment detection 3. **Error Handling**: Implement graceful fallbacks when mock generation fails 4. **Bundle Optimization**: Use dynamic imports to prevent production contamination This test infrastructure architecture provides a solid foundation for sustainable, scalable test development while maintaining clean separation of concerns and production safety.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kesslerio/attio-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server