La aparición de arquitecturas de microservicios ha necesitado el patrón Saga para gestionar transacciones distribuidas a través de límites de servicio donde las garantías tradicionales de ACID son imposibles. Históricamente, las pruebas dependían de bases de datos monolíticas con consistencia inmediata, pero los sistemas poliglotas modernos requieren la validación de flujos de trabajo asíncronos y lógica de compensación. El problema principal es que las pruebas de integración convencionales suponen respuestas sincrónicas, sin capturar condiciones de carrera, particiones de red y los estados ambiguos que ocurren cuando algunos participantes de la saga confirman mientras otros fallan.
La solución requiere un enfoque de Ingeniería del Caos integrado en el arnés de pruebas. Arquitectura un marco utilizando Testcontainers para orquestar instancias reales de PostgreSQL, MongoDB y Redis dentro de redes Docker aisladas. Introduce Toxiproxy como un proxy TCP programable entre los servicios para inyectar latencia, restricciones de ancho de banda y particiones de red en pasos precisos de la saga. Emplea Awaitility para afirmaciones asíncronas basadas en sondeo en lugar de esperas estáticas, e integra Jaeger para trazado distribuido para reconstruir rutas de ejecución exactas. Implementa seguimiento de claves de idempotencia basado en UUID para verificar la semántica de compensaciones de exactamente una vez, y construye un GlobalConsistencyValidator que captura estados a través de todas las capas de persistencia para verificar la preservación de invariantes.
Contexto: Una plataforma de comercio electrónico multinacional procesaba pedidos a través de una saga impulsada por eventos que involucraba el Servicio de Inventario (PostgreSQL), el Servicio de Pagos (MongoDB para registros de transacciones) y el Servicio de Envío (Elasticsearch). La arquitectura utilizaba Apache Kafka para la coreografía entre microservicios basados en Java.
Descripción del Problema: Durante el tráfico máximo, la intermitencia de la red provocó que el procesamiento de pagos tuviera éxito mientras que la reserva de inventario fallaba, activando compensación. Sin embargo, la lógica de compensación contenía una condición de carrera crítica donde se emitían solicitudes de reembolso duplicadas si la solicitud de reembolso inicial agotaba el tiempo, violando los contratos de idempotencia. Además, los retrasos en la consistencia eventual a través de los almacenes poliglotas causaron falsos positivos en pruebas existentes que afirmaban una restauración inmediata del inventario, lo que llevó a pipelines CI/CD inestables y defectos no detectados donde se cobraba a los clientes por artículos no disponibles.
Enfoque 1: Pruebas de extremo a extremo basadas en UI con retrasos fijos
Inicialmente consideramos usar Selenium WebDriver para simular flujos de pago de usuario e insertar Thread.sleep(5000) para esperar el procesamiento asíncrono.
Pros: Simple de implementar, cubre el viaje completo del usuario y no requiere cambios en el código del servicio.
Contras: Extremadamente frágil; cinco segundos eran insuficientes bajo carga y excesivos durante períodos inactivos. No se podían inyectar fallas de red en etapas precisas de la saga, haciendo imposible reproducir la condición de carrera específica. El enfoque no ofreció visibilidad sobre los patrones de comunicación HTTP interservicios o transiciones de estado en la base de datos.
Enfoque 2: Pruebas unitarias simuladas con bases de datos en memoria La segunda opción consistió en simular todas las llamadas de servicios externos utilizando Mockito y usar H2 en una base de datos en memoria para las pruebas unitarias de cada servicio. Pros: Tiempo de ejecución inferior a 10 segundos, sin dependencias de infraestructura y resultados deterministas en aislamiento. Contras: No logró detectar problemas de serialización del mundo real, comportamientos de tiempo de espera de sockets TCP o mecanismos de bloqueo específicos de bases de datos presentes en PostgreSQL pero no en H2. La condición de carrera de idempotencia solo se manifestó con el comportamiento real de paquetes de red y agotamiento de conexiones en el pool, lo cual las simulaciones no pueden replicar.
Enfoque 3: Caos Orquestado con Infraestructura Real (Elegido) Implementamos un arnés de prueba dedicado utilizando JUnit 5 y Testcontainers. Cada servicio se ejecutó en contenedores Docker aislados con Toxiproxy gestionando todos los enlaces de red entre ellos. Usamos RestAssured para puntos de entrada de API y WireMock para simular el comportamiento de idempotencia del procesador de pagos externo. Pros: Permite la inyección de fallos precisos en pasos específicos de la saga (por ejemplo, cortando la conexión después de la confirmación de pago pero antes de la verificación del inventario). Awaitility permitió esperar dinámicamente la consistencia eventual sin retrasos fijos. Las trazas de Jaeger proporcionaron análisis forense de rutas de ejecución para verificar rutas de compensación. Contras: Mayor complejidad de configuración inicial y requisitos de recursos (mínimo 8 GB de RAM para la ejecución local), además de un tiempo de inicio más largo en comparación con las pruebas unitarias.
Resultado: El marco detectó el error de idempotencia donde los reintentos de compensación carecían de un manejo apropiado de HTTP 409 Conflicto para claves duplicadas. Después de corregir la lógica para verificar las claves de idempotencia de Redis antes de enviar solicitudes de reembolso, los cargos duplicados en producción cayeron a cero. El tiempo de ejecución de pruebas se redujo de 8 minutos (pruebas de interfaz de usuario inestables) a 45 segundos (pruebas de integración dirigidas) mientras se mejoraba la cobertura de escenarios de fallo en un 300%.
¿Cómo verificas que las transacciones de compensación mantengan la idempotencia cuando las fallas de red causan resultados ambiguos de solicitudes?
Los candidatos normalmente solo afirman los saldos finales de la cuenta, omitiendo la verificación crítica de que los sistemas posteriores recibieron exactamente una solicitud. La implementación correcta implica capturar la clave de idempotencia UUID antes de la inyección de caos, utilizando luego el método verify(exactly(1), postRequestedFor()) de WireMock para confirmar que exactamente una solicitud coincidente llegó a la puerta de enlace de pago. Además, inspeccionar los registros de la máquina de estados del Orquestador de Saga para asegurarse de que las transiciones sigan COMPENSANDO -> COMPENSADO sin estados intermedios FALLIDOS que puedan activar alertas innecesarias. Esto requiere control a nivel TCP para eliminar conexiones después de que se transmiten los bytes de solicitud pero antes de que lleguen los bytes de respuesta, creando la condición exacta de tiempo de espera ambigua que prueba el manejo de idempotencia.
¿Qué estrategia previene la inestabilidad de las pruebas al afirmar la consistencia eventual a través de almacenes de datos heterogéneos con diferentes latencias de replicación?
La mayoría de los candidatos sugieren sondeos con un tiempo de espera fijo. La solución robusta utiliza Awaitility con retroceso exponencial comenzando en 100 ms, limitado a la latencia de producción en el percentil 99 (por ejemplo, 3 segundos). Crucialmente, implementar un mecanismo de Reloj Global o Reloj Vectorial en las pruebas para capturar marcas de tiempo lógicas a través de PostgreSQL, MongoDB y Redis antes de iniciar la saga. Las afirmaciones luego verifican que las operaciones de lectura devuelvan datos con marcas de tiempo mayores o iguales al tiempo de inicio de la saga. Para escenarios de CQRS, suscribirse a eventos CDC utilizando Debezium incrustado en las pruebas en lugar de sondear bases de datos, reduciendo los tiempos de espera de segundos a milisegundos y eliminando condiciones de carrera entre la afirmación de prueba y la replicación de datos.
¿Cómo detectas estados de ejecución parcial donde algunos participantes de la saga confirmaron mientras otros permanecen pendientes, sin acceder a herramientas de observabilidad de producción?
Los candidatos a menudo omiten la necesidad de seguimiento de Saga en proceso o Registros de Auditoría de Saga accesibles para el arnés de pruebas. La solución requiere inyectar un patrón de Sidecar en los contenedores de prueba que intercepta llamadas gRPC o HTTP a los servicios participantes utilizando Envoy o proxies personalizados. Mantener una Matriz de Estado de Saga en el arnés de pruebas que rastree el estado de cada participante (PENDIENTE, CONFIRMADO, ABORTADO). Cuando Toxiproxy inyecta una partición, consultar esta matriz para verificar que los participantes confirmados coincidan con el estado esperado antes de la falla, mientras que los participantes abortados no muestran efectos secundarios. Usar afirmaciones de JSONPath en etiquetas de span de Jaeger para confirmar que las rutas de compensación se ejecuten solo para los participantes confirmados, asegurando que no se liberen recursos para transacciones que nunca se reservaron realmente.