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:


🎨 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:


⚡ 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

2. Service Coordination

3. Event System Consistency

4. Permission Handling

5. Error Handling


📚 References

RxJS Operators

Testing Tools


Last Updated: 2025-10-15 Framework: Angular 20 Test Framework: Vitest 3.x