Visión General de Arquitectura Event-Driven

En la arquitectura event-driven (EDA), los servicios se comunican produciendo y consumiendo eventos en lugar de hacer llamadas API directas. Cuando algo sucede en el Servicio A (un usuario realiza un pedido), publica un evento. Otros servicios reaccionan a ese evento independientemente.

Este enfoque permite acoplamiento débil, alta escalabilidad y resiliencia — pero introduce desafíos de testing que no existen en sistemas sincrónicios: consistencia eventual, orden de eventos, entrega duplicada y modos de falla complejos.

Event Sourcing

En lugar de almacenar el estado actual de una entidad, event sourcing almacena la secuencia de eventos que llevaron a ese estado.

Enfoque tradicional (basado en estado):

Pedido #123: status=enviado, total=99.99, items=[A, B]

Enfoque event sourcing:

Evento 1: OrderCreated { orderId: 123, items: [A, B] }
Evento 2: PaymentReceived { orderId: 123, amount: 99.99 }
Evento 3: OrderShipped { orderId: 123, trackingNumber: "ABC123" }

El estado actual se obtiene reproduciendo todos los eventos en orden.

Testing de Event Sourcing

Test 1: Reconstrucción de estado Dada una secuencia de eventos, verifica que el estado actual del aggregate sea correcto.

test('estado del pedido después de creación y pago', () => {
  const events = [
    { type: 'OrderCreated', data: { orderId: '123', items: ['A', 'B'], total: 99.99 } },
    { type: 'PaymentReceived', data: { orderId: '123', amount: 99.99 } },
  ];

  const order = replayEvents(new Order(), events);

  expect(order.status).toBe('paid');
  expect(order.total).toBe(99.99);
  expect(order.items).toEqual(['A', 'B']);
});

Test 2: El orden de eventos importa Aplicar eventos en diferente orden debe producir estados diferentes (o ser rechazado).

Test 3: Consistencia de snapshots Si el sistema usa snapshots, verifica que estado desde snapshot + eventos subsiguientes sea igual al estado de reproducir todos los eventos.

CQRS (Command Query Responsibility Segregation)

CQRS separa el modelo de escritura (commands) del modelo de lectura (queries). Los commands producen eventos que actualizan el write store. Un projector procesa eventos para actualizar vistas optimizadas para lectura.

Command → Write Model → Events → Projector → Read Model

Testing de CQRS

Probar el lado de commands:

  • Enviar un command válido produce los eventos esperados.
  • Enviar un command inválido es rechazado con el error correcto.
  • Los commands aplican reglas de negocio (no puede enviar un pedido no pagado).

Probar el lado de lectura (projections):

  • Después de procesar eventos, el read model refleja el estado correcto.
  • Las projections manejan eventos fuera de orden de forma elegante.
  • Reconstruir projections produce el mismo resultado que actualizaciones incrementales.

Probar la brecha de consistencia: Después de que un command tiene éxito, hay un delay antes de que el read model se actualice.

Patrón Saga

Los sagas coordinan transacciones distribuidas entre servicios. Hay dos tipos:

Saga Basada en Coreografía

Cada servicio escucha eventos y decide qué hacer. Sin coordinador central.

OrderService: OrderCreated →
PaymentService: (escucha) → PaymentProcessed →
InventoryService: (escucha) → StockReserved →
ShippingService: (escucha) → ShipmentCreated

Testing de coreografía:

  1. Dispara el evento inicial y verifica que toda la cadena se complete.
  2. Simula una falla en cada paso y verifica que se disparen eventos compensatorios.
  3. Prueba escenarios de timeout — ¿qué pasa si un servicio no responde?

Saga Basada en Orquestación

Un orquestador central coordina los pasos de la saga.

Testing de orquestación:

  1. Happy path: Todos los pasos exitosos → saga completa.
  2. Falla en cada paso: Verifica que las acciones compensatorias se ejecuten correctamente.
  3. Crash/reinicio del orquestador: Verifica que la saga se reanude donde se quedó.

Testing de Consistencia Eventual

La parte más difícil de probar sistemas event-driven es verificar que los servicios eventualmente alcancen un estado consistente.

Patrón de Polling

async function waitForConsistency(checkFn, timeout = 10000, interval = 500) {
  const start = Date.now();
  while (Date.now() - start < timeout) {
    const result = await checkFn();
    if (result) return result;
    await new Promise(resolve => setTimeout(resolve, interval));
  }
  throw new Error('Timeout de consistencia excedido');
}

test('pedido aparece en read model después de creación', async () => {
  await commandBus.send(new CreateOrderCommand({ items: ['A'] }));

  const order = await waitForConsistency(async () => {
    const result = await readModel.getOrder('123');
    return result?.status === 'created' ? result : null;
  });

  expect(order.items).toEqual(['A']);
});

Testing de Idempotencia

Verifica que procesar el mismo evento dos veces produce el mismo resultado:

test('evento de pago es idempotente', async () => {
  const event = { type: 'PaymentReceived', orderId: '123', amount: 99.99 };

  await processEvent(event);
  await processEvent(event); // entrega duplicada

  const payments = await db.getPayments('123');
  expect(payments.length).toBe(1); // no 2
  expect(payments[0].amount).toBe(99.99);
});

Evolución de Schema de Eventos

A medida que los sistemas evolucionan, los schemas de eventos cambian. Los consumidores deben manejar formatos viejos y nuevos.

Ejercicio: Testing de Sistema Event-Driven

Diseña y ejecuta tests para un sistema e-commerce event-driven.

Descripción del Sistema

El sistema tiene cuatro servicios comunicándose vía eventos:

OrderService → PaymentService → InventoryService → NotificationService

Eventos:

  1. OrderCreated — publicado cuando un cliente realiza un pedido
  2. PaymentProcessed — publicado cuando el pago se confirma
  3. PaymentFailed — publicado cuando el pago se rechaza
  4. StockReserved — publicado cuando el inventario se reserva
  5. StockInsufficient — publicado cuando el inventario no está disponible
  6. OrderCancelled — evento compensatorio cuando un paso falla
  7. RefundInitiated — evento compensatorio cuando el pago necesita reversión

Tarea 1: Test de Happy Path

Escribe un test que verifique el flujo completo:

  1. Publica OrderCreated con detalles del pedido.
  2. Espera que aparezca PaymentProcessed.
  3. Espera que aparezca StockReserved.
  4. Verifica que el estado del pedido en el read model sea “confirmed”.
  5. Verifica que el inventario fue decrementado.

Tarea 2: Tests de Compensación

Para cada punto de falla, verifica que la saga haga rollback correctamente:

Escenario A: Falla de pago

  1. Publica OrderCreated.
  2. PaymentService publica PaymentFailed.
  3. Verifica que se publique OrderCancelled.
  4. Verifica que el estado del pedido sea “cancelled” en el read model.

Escenario B: Stock insuficiente

  1. Publica OrderCreated.
  2. PaymentService publica PaymentProcessed.
  3. InventoryService publica StockInsufficient.
  4. Verifica que se publique RefundInitiated (compensando el pago).
  5. Verifica que se publique OrderCancelled.

Tarea 3: Test de Idempotencia

  1. Publica el mismo evento OrderCreated dos veces (simulando entrega duplicada).
  2. Verifica que solo se cree un pedido en la base de datos.
  3. Verifica que solo se intente un pago.

Tarea 4: Test de Orden de Eventos

  1. Publica PaymentProcessed antes de OrderCreated (fuera de orden).
  2. Verifica que el sistema lo maneje de forma elegante.

Tarea 5: Verificación de Consistencia Eventual

  1. Crea un pedido vía el endpoint de command.
  2. Consulta inmediatamente el read model — puede no estar actualizado aún.
  3. Hace polling al read model hasta que aparezca el pedido (con timeout).
  4. Registra el delay de consistencia.
  5. Repite 10 veces y calcula el delay promedio de consistencia.

Entregables

  1. Código de test para cada escenario con assertions.
  2. Un reporte de tiempos mostrando delays de consistencia eventual.
  3. Documentación de condiciones de carrera o edge cases descubiertos.