integration-testing-patterns.md

Comprehensive integration testing patterns for micro-block architecture with real service validation

integration-testing-patterns.mdv1.0.014.9 KB
integration-testing-patterns.md(markdown)
1# Integration Testing Patterns for Micro-Block Architecture
2
3## Overview
4
5Integration testing is the primary validation method for micro-block commands. Unlike unit tests that mock dependencies, integration tests connect to real services and test actual functionality end-to-end.
6
7## Integration Testing Philosophy
8
9**Integration tests are preferred over unit tests** for validating command implementations because:
10
111. **Real Service Validation**: Tests execute against actual services, ensuring they work correctly
122. **Full System Testing**: All dependencies work together as they would in production
133. **Data Integrity Verification**: Tests verify that actual database schema, constraints, and relationships work
144. **Environment Configuration Testing**: Tests validate that connection strings, configuration, and service setup work properly
155. **Contract Compliance**: Tests verify that services actually implement their declared interfaces
16
17## Core Testing Patterns
18
19### Test Structure Pattern
20
21```typescript
22describe('Command Integration Tests', () => {
23  let serviceRegistry: ServiceRegistry;
24  let commandRegistry: CommandRegistry;
25  let logger: ConsoleLogger;
26
27  beforeAll(async () => {
28    // Load real environment configuration
29    dotenv.config({ path: path.resolve(process.cwd(), '.env.local') });
30    
31    // Initialize ServiceRegistry with real configuration
32    serviceRegistry = ServiceRegistry.getInstance();
33    await serviceRegistry.initialize();
34    
35    // Get CommandRegistry through ServiceRegistry
36    commandRegistry = serviceRegistry.getCommandRegistry();
37    
38    // Create logger for test output
39    logger = new ConsoleLogger('IntegrationTest');
40    
41    // Verify critical services are healthy
42    const databaseService = serviceRegistry.get<IDatabaseService>('IDatabaseService');
43    const isHealthy = await databaseService.isHealthy();
44    if (!isHealthy) {
45      throw new Error('Database service is not healthy - cannot run integration tests');
46    }
47  });
48
49  afterAll(async () => {
50    // Cleanup resources
51    await serviceRegistry.destroy();
52  });
53});
54```
55
56### Command Testing Pattern
57
58```typescript
59describe('CreateUserCommand Integration', () => {
60  it('should create user with real database', async () => {
61    // 1. Get command through CommandRegistry (not direct instantiation)
62    const { CreateUserCommand } = await import('@/commands/user/CreateUserCommand');
63    const command = await commandRegistry.get(CreateUserCommand, {
64      email: 'integration-test@example.com',
65      password: 'password123',
66      name: 'Integration Test User'
67    }, logger);
68    
69    // 2. Execute against real services
70    const result = await command.execute();
71    
72    // 3. Verify results with real data
73    expect(result.userId).toBeDefined();
74    expect(result.email).toBe('integration-test@example.com');
75    expect(result.createdAt).toBeInstanceOf(Date);
76    
77    // 4. Verify in database (optional verification)
78    const databaseService = serviceRegistry.get<IDatabaseService>('IDatabaseService');
79    const users = await databaseService.query(
80      'SELECT * FROM Users WHERE email = @email',
81      { email: 'integration-test@example.com' }
82    );
83    expect(users).toHaveLength(1);
84    
85    // 5. Cleanup test data
86    await databaseService.execute(
87      'DELETE FROM Users WHERE email = @email',
88      { email: 'integration-test@example.com' }
89    );
90  });
91  
92  it('should handle validation errors appropriately', async () => {
93    const { CreateUserCommand } = await import('@/commands/user/CreateUserCommand');
94    
95    // Test with invalid input
96    await expect(async () => {
97      const command = await commandRegistry.get(CreateUserCommand, {
98        email: 'invalid-email',  // Invalid format
99        password: '123',         // Too short
100        name: ''                 // Empty name
101      }, logger);
102      
103      await command.execute();
104    }).rejects.toThrow('Email must be a valid email address format');
105  });
106});
107```
108
109### Service Testing Pattern
110
111```typescript
112describe('DatabaseService Integration', () => {
113  let databaseService: IDatabaseService;
114  
115  beforeEach(() => {
116    databaseService = serviceRegistry.get<IDatabaseService>('IDatabaseService');
117  });
118  
119  it('should implement IDatabaseService contract', () => {
120    expect(databaseService).toBeDefined();
121    expect(typeof databaseService.query).toBe('function');
122    expect(typeof databaseService.execute).toBe('function');
123    expect(typeof databaseService.isHealthy).toBe('function');
124  });
125  
126  it('should connect to real database', async () => {
127    const isHealthy = await databaseService.isHealthy();
128    expect(isHealthy).toBe(true);
129  });
130  
131  it('should execute queries against real database', async () => {
132    const result = await databaseService.query('SELECT 1 as test');
133    expect(result).toHaveLength(1);
134    expect(result[0].test).toBe(1);
135  });
136  
137  it('should handle transactions properly', async () => {
138    await databaseService.transaction(async (tx) => {
139      await tx.execute('INSERT INTO TestTable (name) VALUES (@name)', { name: 'test' });
140      const rows = await tx.query('SELECT * FROM TestTable WHERE name = @name', { name: 'test' });
141      expect(rows).toHaveLength(1);
142      
143      // Transaction will rollback automatically for test cleanup
144      throw new Error('Rollback test transaction');
145    }).catch(error => {
146      expect(error.message).toBe('Rollback test transaction');
147    });
148  });
149});
150```
151
152### Workflow Testing Pattern
153
154```typescript
155describe('User Creation Workflow Integration', () => {
156  it('should execute complete user creation workflow', async () => {
157    // Step 1: Validate user data
158    const { ValidateUserDataCommand } = await import('@/commands/user/ValidateUserDataCommand');
159    const validateCommand = await commandRegistry.get(ValidateUserDataCommand, {
160      email: 'workflow-test@example.com',
161      password: 'securePassword123'
162    }, logger);
163    
164    const validationResult = await validateCommand.execute();
165    expect(validationResult.isValid).toBe(true);
166    
167    // Step 2: Create user account
168    const { CreateUserCommand } = await import('@/commands/user/CreateUserCommand');
169    const createCommand = await commandRegistry.get(CreateUserCommand, {
170      email: 'workflow-test@example.com',
171      password: 'securePassword123',
172      name: 'Workflow Test User'
173    }, logger);
174    
175    const createResult = await createCommand.execute();
176    expect(createResult.userId).toBeDefined();
177    
178    // Step 3: Send welcome email
179    const { SendWelcomeEmailCommand } = await import('@/commands/email/SendWelcomeEmailCommand');
180    const emailCommand = await commandRegistry.get(SendWelcomeEmailCommand, {
181      userId: createResult.userId,
182      email: createResult.email,
183      name: 'Workflow Test User'
184    }, logger);
185    
186    const emailResult = await emailCommand.execute();
187    expect(emailResult.emailSent).toBe(true);
188    
189    // Cleanup
190    const databaseService = serviceRegistry.get<IDatabaseService>('IDatabaseService');
191    await databaseService.execute(
192      'DELETE FROM Users WHERE email = @email',
193      { email: 'workflow-test@example.com' }
194    );
195  });
196});
197```
198
199## Testing Service Implementations
200
201### Multiple Implementation Testing
202
203```typescript
204describe('Cache Service Implementations', () => {
205  const testData = { key: 'test-key', value: { data: 'test-value', timestamp: Date.now() } };
206  
207  // Test all cache implementations
208  const implementations = ['MemoryCacheService', 'RedisCacheService'];
209  
210  implementations.forEach(implementationName => {
211    describe(`${implementationName} Integration`, () => {
212      let cacheService: ICacheService;
213      
214      beforeEach(() => {
215        // Get the specific implementation from registry
216        cacheService = serviceRegistry.get<ICacheService>(`I${implementationName}`);
217      });
218      
219      afterEach(async () => {
220        // Clean up test data
221        await cacheService.flush();
222      });
223      
224      it('should implement ICacheService contract', () => {
225        expect(cacheService).toBeDefined();
226        expect(typeof cacheService.get).toBe('function');
227        expect(typeof cacheService.set).toBe('function');
228        expect(typeof cacheService.delete).toBe('function');
229        expect(typeof cacheService.flush).toBe('function');
230      });
231      
232      it('should store and retrieve data', async () => {
233        await cacheService.set(testData.key, testData.value);
234        const retrieved = await cacheService.get(testData.key);
235        
236        expect(retrieved).toEqual(testData.value);
237      });
238      
239      it('should handle TTL expiration', async () => {
240        await cacheService.set(testData.key, testData.value, 1); // 1 second TTL
241        
242        // Immediate retrieval should work
243        let retrieved = await cacheService.get(testData.key);
244        expect(retrieved).toEqual(testData.value);
245        
246        // Wait for expiration
247        await new Promise(resolve => setTimeout(resolve, 1100));
248        
249        // Should be expired
250        retrieved = await cacheService.get(testData.key);
251        expect(retrieved).toBeNull();
252      });
253    });
254  });
255});
256```
257
258## Error Handling Testing
259
260### Error Contract Testing
261
262```typescript
263describe('Command Error Handling Integration', () => {
264  it('should throw properly structured errors', async () => {
265    const { CreateUserCommand } = await import('@/commands/user/CreateUserCommand');
266    
267    try {
268      const command = await commandRegistry.get(CreateUserCommand, {
269        email: 'invalid-email',
270        password: 'short',
271        name: ''
272      }, logger);
273      
274      await command.execute();
275      fail('Command should have thrown validation error');
276    } catch (error) {
277      expect(error).toBeInstanceOf(Error);
278      expect(error.name).toBe('CreateUserError');
279      expect(error.code).toBe('VALIDATION_ERROR');
280      expect(error.invalidInput).toBe(true);
281      expect(error.invalidInputName).toBeDefined();
282      expect(error.invalidInputPath).toBeDefined();
283      expect(error.timestamp).toBeInstanceOf(Date);
284    }
285  });
286  
287  it('should handle service unavailability gracefully', async () => {
288    // Temporarily make database unhealthy
289    const databaseService = serviceRegistry.get<IDatabaseService>('IDatabaseService');
290    const originalIsHealthy = databaseService.isHealthy;
291    databaseService.isHealthy = async () => false;
292    
293    try {
294      const { CreateUserCommand } = await import('@/commands/user/CreateUserCommand');
295      const command = await commandRegistry.get(CreateUserCommand, {
296        email: 'test@example.com',
297        password: 'password123',
298        name: 'Test User'
299      }, logger);
300      
301      await command.execute();
302      fail('Command should have thrown service error');
303    } catch (error) {
304      expect(error.code).toBe('SERVICE_UNHEALTHY');
305    } finally {
306      // Restore original method
307      databaseService.isHealthy = originalIsHealthy;
308    }
309  });
310});
311```
312
313## Performance Testing
314
315### Performance Benchmarking
316
317```typescript
318describe('Command Performance Integration', () => {
319  it('should execute within acceptable time limits', async () => {
320    const { GetUserCommand } = await import('@/commands/user/GetUserCommand');
321    
322    const startTime = Date.now();
323    
324    const command = await commandRegistry.get(GetUserCommand, {
325      userId: 'existing-user-id'
326    }, logger);
327    
328    const result = await command.execute();
329    
330    const executionTime = Date.now() - startTime;
331    
332    expect(result).toBeDefined();
333    expect(executionTime).toBeLessThan(1000); // Should complete within 1 second
334    
335    logger.info(`GetUserCommand executed in ${executionTime}ms`);
336  });
337  
338  it('should handle concurrent executions', async () => {
339    const { GetUserCommand } = await import('@/commands/user/GetUserCommand');
340    const userIds = ['user1', 'user2', 'user3', 'user4', 'user5'];
341    
342    const startTime = Date.now();
343    
344    // Execute commands concurrently
345    const promises = userIds.map(async (userId) => {
346      const command = await commandRegistry.get(GetUserCommand, { userId }, logger);
347      return command.execute();
348    });
349    
350    const results = await Promise.all(promises);
351    
352    const totalTime = Date.now() - startTime;
353    
354    expect(results).toHaveLength(5);
355    expect(totalTime).toBeLessThan(2000); // Concurrent execution should be faster than sequential
356    
357    logger.info(`Concurrent execution completed in ${totalTime}ms`);
358  });
359});
360```
361
362## Test Data Management
363
364### Test Data Setup Pattern
365
366```typescript
367describe('Integration Test with Test Data', () => {
368  let testUserId: string;
369  let testArtifactId: string;
370  
371  beforeEach(async () => {
372    // Setup test data
373    const { CreateUserCommand } = await import('@/commands/user/CreateUserCommand');
374    const createUserCommand = await commandRegistry.get(CreateUserCommand, {
375      email: `test-${Date.now()}@example.com`,
376      password: 'password123',
377      name: 'Test User'
378    }, logger);
379    
380    const userResult = await createUserCommand.execute();
381    testUserId = userResult.userId;
382    
383    const { CreateArtifactCommand } = await import('@/commands/artifact/CreateArtifactCommand');
384    const createArtifactCommand = await commandRegistry.get(CreateArtifactCommand, {
385      userId: testUserId,
386      name: 'Test Artifact',
387      content: 'Test content'
388    }, logger);
389    
390    const artifactResult = await createArtifactCommand.execute();
391    testArtifactId = artifactResult.artifactId;
392  });
393  
394  afterEach(async () => {
395    // Cleanup test data
396    const databaseService = serviceRegistry.get<IDatabaseService>('IDatabaseService');
397    
398    await databaseService.execute(
399      'DELETE FROM Artifacts WHERE id = @artifactId',
400      { artifactId: testArtifactId }
401    );
402    
403    await databaseService.execute(
404      'DELETE FROM Users WHERE id = @userId',
405      { userId: testUserId }
406    );
407  });
408  
409  it('should test with fresh test data', async () => {
410    // Test implementation using testUserId and testArtifactId
411  });
412});
413```
414
415## Best Practices
416
417### Integration Test Design
418
4191. **Test Real Integrations**: Use actual services, not mocks
4202. **Test Complete Workflows**: Verify end-to-end functionality
4213. **Verify Error Handling**: Test error scenarios with real services
4224. **Performance Awareness**: Monitor execution times
4235. **Data Cleanup**: Always clean up test data
424
425### Test Organization
426
4271. **Separate by Category**: Group tests by command category
4282. **Service Contract Testing**: Verify service implementations
4293. **Workflow Testing**: Test command compositions
4304. **Error Scenario Coverage**: Test all error paths
4315. **Performance Benchmarking**: Include performance tests
432
433### Environment Setup
434
4351. **Real Configuration**: Use actual environment variables
4362. **Service Health Checks**: Verify services before testing
4373. **Database Transactions**: Use transactions for data isolation
4384. **Resource Cleanup**: Properly cleanup resources after tests
4395. **Logging**: Include detailed logging for debugging
440
441This integration testing approach ensures that micro-block commands work correctly with real services and data, providing confidence in the system's reliability and performance.

Metadata

Path
utaba/main/patterns/testing/integration-testing-patterns.md
Namespace
utaba/main/patterns/testing
Author
utaba
Category
patterns
Technology
testing
Contract Version
1.0.0
MIME Type
text/markdown
Published
18-Jul-2025
Last Updated
18-Jul-2025