La Prueba Que Pasó. El Incidente de Producción Que No.
Son las 3 AM. Tu servicio está lanzando errores 503. El circuit breaker del que estás seguro que funciona correctamente — porque tiene pruebas — no está abriendo. Tu lógica de reintento, también probada, está amplificando el problema 9 veces. Tu resolvedor de DNS está devolviendo EAI_AGAIN contra un clúster de CoreDNS sobrecargado, y cada reintento empeora la situación.
Haces un rollback. El incidente se cierra. Escribes el post-mortem.
Seis semanas después, vuelves a desplegar. El mismo modo de fallo, con una forma ligeramente diferente. Las pruebas siguen pasando.
Hemos visto este patrón docenas de veces. Las pruebas no están equivocadas. El comportamiento probado es real. Pero existe una categoría de fallo que ninguna cantidad de pruebas unitarias, de integración o de contenedor capturará jamás: qué ocurre cuando la infraestructura bajo tu aplicación empieza a comportarse de maneras que son técnicamente válidas pero operacionalmente hostiles. ¿Qué pasa cuando el DNS empieza a oscilar? ¿Cuando las escrituras de red devuelven valores cortos? ¿Cuando tu pool de conexiones se drena a cero, no por un bug, sino por carga coordinada?
Esa es la brecha. Y es por eso que construimos una pila de pruebas de caos.
Las Cuatro Fases de las Pruebas Modernas
La mayoría de los equipos de ingeniería piensan en las pruebas en tres fases. Nosotros argumentamos que son cuatro, y que saltarse la cuarta es lo que hace posibles los incidentes nocturnos.
Phase 1: Unit Tests
└── Tests a single unit of logic in isolation
└── Fast, precise, tells you the code is correct
└── Cannot tell you the system handles failure
Phase 2: Integration Tests
└── Tests interactions between components
└── Catches interface mismatches and contract violations
└── Still uses predictable, cooperative dependencies
Phase 3: Container Tests (Testcontainers, Docker Compose)
└── Runs real databases, brokers, caches
└── Validates real-system behavior with real dependencies
└── Dependencies are healthy, well-behaved, and available
Phase 4: Chaos Tests ← the missing phase
└── Injects controlled failures into real system components
└── Validates recovery logic, circuit breakers, retry strategies
└── Proves your system handles the unexpected, not just the expected Lo Que las Tres Primeras Fases No Pueden Probar
Seamos específicos sobre la brecha. Esto es lo que las Fases 1–3 no pueden validar:
**Tu circuit breaker realmente se abre.** Puedes hacer pruebas unitarias de la configuración de Resilience4j, pero el circuit breaker no abrirá a menos que la llamada downstream falle realmente a la tasa correcta, en el momento correcto, con el tipo de excepción correcto. Los mocks hacen el fallo demasiado limpio.
**Tu lógica de reintento no amplifica el fallo.** Un reintento 5x en tres réplicas contra un backend fallando no es una amplificación 5x — es 15x. Esto es invisible en las pruebas unitarias. Es invisible en las pruebas de integración a menos que ingenierices cuidadosamente fallos parciales.
**Tu cliente DNS maneja fallos transitorios de resolución.** getaddrinfo() devolviendo EAI_AGAIN es una respuesta POSIX válida y esperada. La mayoría de los servicios nunca prueban este camino. El resultado: un problema puntual de CoreDNS se convierte en una interrupción de 30 segundos.
**Tu pool de conexiones se recupera bajo presión.** El agotamiento del pool seguido de una recuperación parcial es una máquina de estados sutil. La lógica de recuperación generalmente se escribe una vez, nunca se prueba, y se descubre en producción.
**Tu pila de timeouts se compone correctamente.** Un timeout de lectura de 30s detrás de un timeout de cliente HTTP de 10s detrás de un timeout de balanceador de carga de 5s — el comportamiento compuesto no es lo que ninguna prueba individual ejercita.
Presentando la Pila de Pruebas de Caos
Construimos tres bibliotecas de código abierto que juntas cubren toda la superficie de las pruebas de caos de Fase 4.
**Biblioteca 1 — Caos C99 LD_PRELOAD (`chaos-testing-libraries`):** Un conjunto de objetos compartidos que interceptan llamadas libc a nivel del enlazador dinámico. Agnóstico al lenguaje — funciona en cualquier binario ELF: Java, Go, Python, Node.js, Rust con CGO. Seis dominios de fallo: E/S de ficheros, red, DNS, reloj, memoria y ciclo de vida de procesos. No se requieren cambios en el código de la aplicación. La interfaz completa es un fichero de configuración de texto plano.
**Biblioteca 2 — Agente de Caos JVM (`chaos-testing-java-agent`):** Un agente Java que instrumenta 62 sitios de llamada del JDK usando ByteBuddy. Inyecta fallos, retrasos y presión de recursos a nivel del JVM — dentro de HikariCP, dentro de Netty, dentro de las propias pilas SSL y DNS del JDK. Aislamiento por sesión por prueba, para que múltiples pruebas de caos puedan ejecutarse concurrentemente en el mismo JVM sin interferirse entre sí.
**Biblioteca 3 — Framework de Pruebas Java (`chaos-testing`):** Un sistema de extensión JUnit 5 con 448 anotaciones L1 (primitivas de syscall sin procesar), 92 composites L2 (patrones de fallo documentados) y 64 escenarios L3 (incidentes reales de producción reproducidos como anotaciones individuales). Se integra con Spring Boot 3/4, Quarkus y Micronaut. Gestiona la inyección de LD_PRELOAD en contenedores Docker automáticamente a través de la API de Docker.
Las tres comparten la misma gramática DSL de selector × efecto × probabilidad. Aprendes el modelo mental una vez. Lo aplicas en cada capa.
Capa 1: Fallos Reales del Kernel para Cualquier Proceso
El conjunto de bibliotecas C99 opera en el nivel más bajo disponible para el código de espacio de usuario: el límite de libc. Cuando tu proceso Java llama a read() en un socket, pasa por glibc. Cuando tu script Python llama a socket.connect(), pasa por glibc. Ahí es donde interceptamos — antes del kernel, después de tu código.
Seis objetos compartidos independientes, cada uno propietario de un conjunto disjunto de símbolos para que puedan componerse sin conflicto:
- `libchaos-io.so` — lectura/escritura de ficheros, fsync, escrituras rotas (retornos cortos), corrupción de lectura de un solo bit - `libchaos-net.so` — connect, send, recv, accept, con inyección de ERRNO, latencia y corrupción de carga útil - `libchaos-dns.so` — getaddrinfo/getnameinfo, con reescritura de nombres, EAI_AGAIN, EAI_FAIL, barajado y limitación de respuestas - `libchaos-time.so` — clock_gettime, nanosleep con sesgo de tiempo con signo e inyección de latencia - `libchaos-memory.so` — mmap/munmap/mprotect con inyección de ENOMEM a probabilidad configurable - `libchaos-process.so` — fork, pthread_create, posix_spawn, exec — semántica de fallo-después-de-N y latencia
La interfaz es un fichero de configuración de texto plano en una ruta bien conocida. Sin demonios, sin APIs, sin agentes que ejecutar. Escribe una configuración, precarga la biblioteca, ejecuta tu proceso.
# /tmp/.chaos-net.conf — 10% connection failures to your downstream
tcp4://payments-service:8080:connect:ECONNREFUSED:0.10
# /tmp/.chaos-dns.conf — transient DNS failures for internal services
dns://*.internal:EAI_AGAIN:0.15
# Inject faults into any process — no code changes
LD_PRELOAD=libchaos-net.so:libchaos-dns.so ./your-service Capa 2: 62 Puntos de Intercepción del JDK
La capa C99 es potente pero gruesa: opera sobre llamadas a nivel de SO. Para cargas de trabajo JVM, necesitas mayor precisión. La diferencia entre que HikariCP agote el timeout al pedir prestada una conexión frente a que una conexión TCP agote el timeout en el kernel es exactamente la distinción que importa para el ajuste de circuit breakers.
El agente JVM instrumenta 62 sitios de llamada del JDK usando ByteBuddy con inlining de @Advice. Después del calentamiento JIT, el dispatch de caos se compila hasta una comprobación nula y una rama no tomada — sobrecarga casi nula en el caso común.
Crucialmente: cada prueba obtiene una ChaosSession respaldada por ThreadLocal<UUID>. Los escenarios se registran con ámbito de sesión, disparándose solo en los hilos que llevan el ID de sesión de la prueba. Múltiples pruebas se ejecutan concurrentemente en el mismo JVM sin interferencia — el caos de la prueba A es invisible para la prueba B.
Los starters de Spring Boot 3 y 4 hacen que el cableado sea transparente — una dependencia de prueba, una anotación, y el agente se auto-adjunta:
@SpringBootTest
@ChaosTest
class PaymentServiceChaosTest {
@Test
void circuitBreaker_opens_on_jdbcPoolExhaustion(ChaosSession session) {
try (var h = session.activate(
ChaosScenario.jdbc()
.reject("chaos: pool exhausted")
.probability(1.0)
.maxApplications(3)
.build())) {
try (var scope = session.bind()) {
// First 3 JDBC borrows fail → circuit must open
assertThat(service.charge(order))
.isEqualTo(PaymentResult.REJECTED_CIRCUIT_OPEN);
}
}
}
} Capa 3: 604 Anotaciones, 64 Incidentes de Producción
El framework Java es lo que hace que las pruebas de caos sean accesibles a nivel de equipo. En lugar de aprender DSLs de fallos y ajustar probabilidades, un desarrollador anota una prueba con el nombre de un patrón de incidente real y el framework gestiona la composición.
**448 anotaciones L1** ofrecen control quirúrgico a nivel de syscall: `@ChaosRecvEconnreset(probability = 0.05)`, `@ChaosDnsGetaddrinfoEaiAgain(hostPattern = "*.internal", probability = 0.20)`, `@ChaosMmapEnomem(probability = 0.001)`
**92 composites L2** agrupan fallos relacionados en patrones documentados: `@CompositeChaosTransientDnsFailure`, `@CompositeChaosConnectionRefused`, `@CompositeChaosLowMemoryPressure`
**64 escenarios L3** codifican incidentes reales de producción como anotaciones individuales — cada una con una calificación de severidad, referencia de fuente y una clase Composer multi-dominio que sabe cómo descomponer el incidente en la mezcla correcta de fallos de syscall:
`@IncidentChaosK8sRollingUpdateRst` — ECONNRESET en el 30% de las llamadas RECV, reproduciendo la ventana de retardo de iptables durante las actualizaciones continuas de Kubernetes
`@IncidentChaosFeignRetryAmplification` — ECONNREFUSED en el 50% de connect() más inyección de IOException a nivel de Feign, reproduciendo el patrón de tormenta de reintentos que crea una amplificación de carga 9x
`@IncidentChaosSpringTransactionalPoolDeadlock` — el patrón de deadlock de @Transactional(REQUIRES_NEW) que drena el pool de conexiones desde dentro
El framework inyecta automáticamente las bibliotecas LD_PRELOAD correctas en tu contenedor Docker antes de que comience la prueba — a través del flujo tar de la API de Docker, sin shell exec, sin sidecar, sin modificación de imagen.
@Testcontainers
@ExtendWith(ChaosTestingExtension.class)
@SyscallLevelChaos(LibchaosLib.NET)
class OrderServiceChaosTest {
@Container @AppContainer
static GenericContainer<?> app =
new GenericContainer<>("order-service:latest")
.withExposedPorts(8080);
@Test
@IncidentChaosK8sRollingUpdateRst(toxicity = 0.3)
void service_handles_rst_during_rolling_deploy() {
// 30% of RECV calls return ECONNRESET — exactly what happens
// during a Kubernetes rolling update when iptables lags behind
assertThat(callService("/api/order/42")).isEqualTo(200);
}
} Un DSL, Tres Capas, Sin Brechas
La decisión de diseño clave es que las tres capas comparten la misma gramática conceptual: selector × efecto × política.
Un **selector** apunta a qué fallar: un prefijo de ruta de fichero, un endpoint TCP, un tipo de operación del JDK, un patrón de nombre DNS. Un **efecto** define cómo fallarlo: inyección de ERRNO, latencia, corrupción de datos, rechazo, corrupción de valor en los límites. Una **política** controla cuándo fallar: probabilidad, límite de tasa, conteo de calentamiento, ventana de tiempo, máximo de aplicaciones.
Ya sea que estés escribiendo un fichero de configuración C99, construyendo un ChaosScenario para el agente JVM, o eligiendo una anotación JUnit 5, estás trabajando con el mismo modelo mental. El desarrollador que entiende `@IncidentChaosFeignRetryAmplification` puede profundizar en las anotaciones L1 subyacentes y leer exactamente qué syscalls se están fallando, a qué tasas y por qué.
Y las tres capas pueden estar activas simultáneamente. Una única ejecución de prueba puede tener: - El contenedor ejecutándose con `libchaos-net.so` inyectado (30% ECONNRESET) - El agente JVM produciendo presión en la caché de código (estresador en segundo plano) - Una ChaosSession con rechazo JDBC activo (los primeros 3 préstamos del pool fallan)
Las tres controladas independientemente, las tres con ámbito correcto, ninguna interfiriendo con las demás.
El Caos Como Ciudadano de Primera Clase en CI
El cambio que defendemos no se trata de Game Days o experimentos de caos en producción. Es más simple y más valioso: **cada PR que toque el manejo de errores, la lógica de reintento, los circuit breakers o la gestión de conexiones debería tener una prueba de caos que bloquee el build**.
Esto reencuadra el chaos engineering de una disciplina de operaciones a una disciplina de desarrollo. La pregunta ya no es «¿puede nuestra plataforma sobrevivir a una partición de red?» — respondida en producción, demasiado tarde. La pregunta se convierte en «¿el circuit breaker de este PR se abre correctamente cuando el pool JDBC se agota, antes de que este código se despliegue?»
Las herramientas existen. Las anotaciones están a un import de distancia. La inyección en Docker es automática. El coste de añadir `@IncidentChaosK8sRollingUpdateRst` a una prueba de integración es el mismo que añadir cualquier otra anotación JUnit. El coste de no tenerla es lo que pagas a las 3 AM.
Por Dónde Empezar
Cada biblioteca tiene su propia entrada en profundidad, pero si quieres pruebas de caos ejecutándose hoy, empieza con el framework Java — tiene la historia de primeros pasos más completa y el impacto más inmediato.
**Paso 1:** Añade la dependencia para tu framework (Spring Boot 3/4, Quarkus o Micronaut). **Paso 2:** Anota tus pruebas de contenedor existentes con @ExtendWith(ChaosTestingExtension.class) y @SyscallLevelChaos. **Paso 3:** Elige una anotación L3 que coincida con un modo de fallo que hayas visto en producción o sobre el que te preocupes. **Paso 4:** Ejecuta la prueba. Obsérvala fallar si tu código de resiliencia aún no está ahí. Corrígelo. Despliégalo con confianza.
La capa de caos no reemplaza tus otras fases de prueba. Las completa.
- chaos-testing-libraries — fallos C99 LD_PRELOAD para cualquier proceso ELF, cualquier lenguaje
- chaos-testing-java-agent — agente de bytecode JVM, 62 sitios de llamada del JDK, aislamiento de sesión por prueba
- chaos-testing — framework JUnit 5, Spring Boot / Quarkus / Micronaut, 64 anotaciones de incidentes
Key Takeaways
La pirámide de pruebas nunca estuvo equivocada — estaba incompleta. Las pruebas unitarias verifican la corrección. Las pruebas de integración verifican los contratos. Las pruebas de contenedor verifican el comportamiento frente a dependencias reales. Las pruebas de caos verifican la supervivencia frente a un entorno hostil.
Construimos tres bibliotecas que cubren esta última milla: un toolkit C99 LD_PRELOAD para interceptación libc real del kernel, un agente de bytecode JVM con 62 puntos de intercepción y aislamiento de sesión por prueba, y un framework de anotaciones JUnit 5 con 64 incidentes reales de producción codificados como escenarios de prueba reutilizables.
La Fase 4 está lista. Tu pipeline de CI está esperando.
Engineering Team
Senior Solutions Architects
Llevamos construyendo sistemas distribuidos desde antes de que 'microservicios' fuera una cosa. Nuestras cicatrices cuentan historias.