Testing Guide
Ecosistema completo para organización y gestión de contenido con interfaces espaciales
Action Registry - Testing Guide
Technical patterns and best practices for testing action-based architectures.
🎯 Observable Stream Patterns
Understanding Stream Completion
Key Concept: Different RxJS operators have different emission and completion behaviors.
// Pattern: Sequential processing with concatMap
return from(items).pipe(
concatMap((item) => {
return processItem(item).pipe(
map((result) => {
// Emits ONCE per item
return result;
})
);
}),
map(() => {
// Runs ONCE after ALL items processed
return aggregatedResult;
})
);
Testing Pattern:
// ❌ Wrong - captures first emission only
const result = await firstValueFrom(processMultipleItems());
// ✅ Right - waits for stream completion
const result = await lastValueFrom(processMultipleItems());
Rule of Thumb:
firstValueFrom→ Single-emission observableslastValueFrom→ Multi-step workflows that emit once after completion
🎨 Event System Testing
Multiple Event Naming Conventions
Different services may use different event naming patterns:
// PascalCase Events
'ActionExecuted'
'ActionExecutionFailed'
// Colon-separated Events
'action:registered'
'action:execution:completed'
Testing Pattern:
const eventSpy = vi.fn();
eventBus.subscribeAll().subscribe(eventSpy);
// Execute operation
await operation();
// Extract event names
const events = eventSpy.mock.calls.map(call => call[0]);
const eventNames = events.map(e => e.eventName);
// Verify all expected events
expect(eventNames).toContain('ActionExecuted');
expect(eventNames).toContain('action:registered');
🔐 Permission Testing Patterns
Null Safety for Permissions
Always handle undefined/null permissions gracefully:
function hasPermissions(
required: string[] | undefined,
userPermissions: string[]
): boolean {
// Handle undefined or empty
if (!required || required.length === 0) {
return true;
}
return required.every(
(perm) =>
userPermissions.includes(perm) ||
userPermissions.includes('*') // Wildcard support
);
}
Testing Pattern:
// Test undefined permissions
expect(hasPermissions(undefined, ['user'])).toBe(true);
// Test empty array
expect(hasPermissions([], ['user'])).toBe(true);
// Test wildcard
expect(hasPermissions(['admin'], ['*'])).toBe(true);
// Test specific permission
expect(hasPermissions(['admin'], ['admin'])).toBe(true);
expect(hasPermissions(['admin'], ['user'])).toBe(false);
🔄 Circuit Breaker Testing
Understanding Circuit Breaker Behavior
Circuit breakers convert errors to results, they don't retry:
// Circuit breaker wraps execution
circuitBreaker.execute(
'operation-id',
() => dangerousOperation(),
{ failureThreshold: 5 }
).pipe(
// Success path
map(result => ({ success: true, data: result })),
// Error path - converts to result object
catchError(error => of({ success: false, error: error.message }))
);
Testing Pattern:
// ✅ Correct - expect error conversion, not retry
it('should handle errors with circuit breaker', async () => {
let attemptCount = 0;
const operation = () => {
attemptCount++;
return throwError(() => new Error('Failed'));
};
const result = await firstValueFrom(
circuitBreaker.execute('test', operation, config)
);
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
expect(attemptCount).toBe(1); // No retry - single attempt
});
State Transitions:
// Test circuit opening after threshold
for (let i = 0; i < 6; i++) {
await executeFailingOperation();
}
const state = circuitBreaker.getState('operation-id');
expect(state).toBe('open');
🧩 Metadata Comparison
Property Augmentation by Services
Services may add properties to objects after registration:
// Input
const metadata = {
id: 'action-1',
name: 'My Action'
};
// After registration
const stored = {
...metadata,
registeredAt: new Date(), // Added by service
version: 1 // Added by service
};
Testing Pattern:
// ❌ Wrong - fails on added properties
expect(retrieved).toEqual(original);
// ✅ Right - compare specific properties
expect(retrieved.id).toBe(original.id);
expect(retrieved.name).toBe(original.name);
// Or use objectContaining
expect(retrieved).toEqual(
expect.objectContaining({
id: original.id,
name: original.name
})
);
🔗 Multi-Service Synchronization
Testing Coordinated Services
When multiple services must stay synchronized:
// Service A - Registration
serviceA.register(item);
// Service B - State Tracking
serviceB.track(item);
// Verify both services updated
expect(serviceA.has(item.id)).toBe(true);
expect(serviceB.count()).toBe(1);
Integration Testing Pattern:
it('should synchronize across services', async () => {
const item = createTestItem();
// Register in all required services
registryService.register(item);
stateService.track(item);
handlerService.prepare(item);
// Verify synchronization
expect(registryService.isRegistered(item.id)).toBe(true);
expect(stateService.getTotal()).toBe(1);
expect(handlerService.isReady(item.id)).toBe(true);
});
📊 Transaction Testing
Testing Transactional Workflows
Transactions execute sequentially and support rollback:
const rollbackSpy = vi.fn(() => of(undefined));
const result = await lastValueFrom(
executeTransaction([
{
id: 'step-1',
params: { data: 'test' },
rollback: rollbackSpy
},
{
id: 'step-2', // This step fails
params: {}
}
])
);
// Verify transaction behavior
expect(result.success).toBe(false);
expect(result.rolledBack).toBe(true);
expect(result.executedCount).toBe(1);
expect(rollbackSpy).toHaveBeenCalled();
Key Assertions:
success: falsewhen any step failsrolledBack: truewhen rollback executedexecutedCountshows steps completed before failure- Rollback functions called in reverse order
⚡ Performance Testing
Metrics Collection Pattern
it('should track performance metrics', async () => {
// Execute operation
await executeOperation();
// Collect metrics
const metrics = metricsService.getMetrics();
expect(metrics.totalOperations).toBeGreaterThan(0);
expect(metrics.successRate).toBeGreaterThan(0);
expect(metrics.averageDuration).toBeGreaterThan(0);
});
Batch Performance Testing
it('should handle batch efficiently', async () => {
const startTime = Date.now();
const result = await firstValueFrom(
executeBatch([
{ id: 'op-1' },
{ id: 'op-2' },
{ id: 'op-3' }
])
);
const duration = Date.now() - startTime;
expect(result.total).toBe(3);
expect(result.successful).toBe(3);
expect(duration).toBeLessThan(500); // Performance threshold
});
🛠️ TestBed Configuration
Standard Setup Pattern
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
ServiceUnderTest,
DependencyA,
DependencyB,
// Mock complex dependencies
{ provide: ComplexService, useValue: mockComplexService }
]
});
service = TestBed.inject(ServiceUnderTest);
});
With HTTP Client
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
TestBed.configureTestingModule({
providers: [
ServiceUnderTest,
provideHttpClient(),
provideHttpClientTesting()
]
});
🔍 Debugging Patterns
Event Flow Debugging
const eventLog: string[] = [];
eventBus.subscribeAll().subscribe(event => {
eventLog.push(`${event.eventName} at ${event.metadata.timestamp}`);
});
// Execute operation
await operation();
// Inspect event sequence
console.log('Event flow:', eventLog);
Result Debugging
const result = await lastValueFrom(complexOperation());
console.log('Result:', JSON.stringify(result, null, 2));
// Helps identify unexpected result structure
📋 Common Test Patterns
AAA Pattern (Arrange-Act-Assert)
it('should perform operation correctly', async () => {
// Arrange - Setup
const input = createTestData();
service.initialize(input);
// Act - Execute
const result = await firstValueFrom(service.execute(input));
// Assert - Verify
expect(result.success).toBe(true);
expect(result.data).toBeDefined();
});
Async Testing with RxJS
// ✅ Preferred: async/await
it('should execute async', async () => {
const result = await firstValueFrom(operation());
expect(result).toBeDefined();
});
// ❌ Avoid: done callback
it('should execute async', (done) => {
operation().subscribe(result => {
expect(result).toBeDefined();
done();
});
});
Error Testing
it('should handle errors gracefully', async () => {
const operation = () => throwError(() => new Error('Test error'));
const result = await firstValueFrom(
service.executeWithErrorHandling(operation)
);
expect(result.success).toBe(false);
expect(result.error).toContain('Test error');
});
🎓 Best Practices
1. Observable Stream Semantics
- Understand emission vs completion behavior
- Choose appropriate stream completion operator
- Test multi-step workflows with proper completion handling
2. Service Coordination
- Don't assume automatic synchronization
- Test each service independently
- Verify integration points explicitly
3. Event System Consistency
- Account for different naming conventions
- Test event emission order
- Verify event payload structure
4. Permission Handling
- Always handle undefined/null cases
- Support wildcard permissions
- Test permission edge cases
5. Error Handling
- Distinguish between thrown errors and error results
- Test circuit breaker behavior correctly
- Don't conflate retry with error handling
📚 References
RxJS Operators
concatMap- Sequential processing with one emission per itemfirstValueFrom- Converts first emission to PromiselastValueFrom- Converts last emission to PromiseforkJoin- Parallel execution, emits when all complete
Testing Tools
- Vitest - Test framework with Angular support
- TestBed - Angular dependency injection for tests
- RxJS Testing -
TestSchedulerfor marble testing
Last Updated: 2025-10-15 Framework: Angular 20 Test Framework: Vitest 3.x