O Teste Passou. O Incidente de Produção Não.
São 3 da manhã. Seu serviço está retornando 503s. O circuit breaker no qual você confia que funciona corretamente — porque possui testes — não está abrindo. Sua lógica de retry, também testada, está amplificando o problema por 9x. Seu resolvedor de DNS está retornando EAI_AGAIN contra um cluster CoreDNS sobrecarregado, e cada retry piora a situação.
Você faz rollback. O incidente é encerrado. Você escreve o post-mortem.
Seis semanas depois, você faz um novo deploy. O mesmo modo de falha, com uma forma ligeiramente diferente. Os testes ainda passam.
Vimos esse padrão dezenas de vezes. Os testes não estão errados. O comportamento testado é real. Mas existe uma categoria de falha que nenhuma quantidade de testes unitários, testes de integração ou testes de contêiner jamais capturará: o que acontece quando a infraestrutura por baixo da sua aplicação começa a se comportar de maneiras tecnicamente válidas, mas operacionalmente hostis. O que acontece quando o DNS começa a oscilar? Quando escritas de rede retornam com dados incompletos? Quando seu pool de conexões esgota até zero, não por um bug, mas por carga coordenada?
Esse é o gap. E é por isso que construímos uma pilha de chaos testing.
As Quatro Fases dos Testes Modernos
A maioria das equipes de engenharia pensa em testes em três fases. Argumentamos que existem quatro, e que pular a quarta é o que torna possíveis os incidentes noturnos.
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 O Que as Três Primeiras Fases Não Conseguem Testar
Vamos ser específicos sobre o gap. Aqui está o que as Fases 1–3 não conseguem validar:
**Seu circuit breaker realmente abre.** Você pode testar unitariamente a configuração do Resilience4j, mas o circuit breaker não abrirá a menos que a chamada downstream realmente falhe na taxa certa, no momento certo, com o tipo de exceção certo. Mocks tornam a falha limpa demais.
**Sua lógica de retry não amplifica falhas.** Um retry de 5x em três réplicas contra um backend falhando não é 5x de amplificação — é 15x. Isso é invisível em testes unitários. É invisível em testes de integração, a menos que você projete cuidadosamente falhas parciais.
**Seu cliente DNS lida com falhas transitórias de resolução.** getaddrinfo() retornando EAI_AGAIN é uma resposta POSIX válida e esperada. A maioria dos serviços nunca testa esse caminho. O resultado: uma instabilidade no CoreDNS se torna uma interrupção de 30 segundos.
**Seu pool de conexões se recupera sob pressão.** Esgotamento do pool seguido de recuperação parcial é uma máquina de estados sutil. A lógica de recuperação geralmente é escrita uma vez, nunca testada, e descoberta em produção.
**Seu stack de timeouts se compõe corretamente.** Um timeout de leitura de 30s atrás de um timeout de cliente HTTP de 10s atrás de um timeout de load balancer de 5s — o comportamento composto não é o que qualquer teste individual exercita.
Apresentando a Pilha de Chaos Testing
Construímos três bibliotecas open-source que juntas cobrem toda a superfície de testes de caos da Fase 4.
**Biblioteca 1 — C99 LD_PRELOAD Chaos (`chaos-testing-libraries`):** Um conjunto de objetos compartilhados que interceptam chamadas libc no nível do linker dinâmico. Agnóstica de linguagem — funciona em qualquer binário ELF: Java, Go, Python, Node.js, Rust com CGO. Seis domínios de falha: I/O de arquivo, rede, DNS, relógio, memória e ciclo de vida de processos. Zero alterações no código da aplicação necessárias. A interface inteira é um arquivo de configuração em texto simples.
**Biblioteca 2 — JVM Chaos Agent (`chaos-testing-java-agent`):** Um agente Java que instrumenta 62 pontos de chamada do JDK usando ByteBuddy. Injeta falhas, atrasos e pressão de recursos no nível do JVM — dentro do HikariCP, dentro do Netty, dentro das próprias pilhas de SSL e DNS do JDK. Isolado por sessão por teste, de modo que múltiplos testes de caos podem rodar concorrentemente no mesmo JVM sem interferir entre si.
**Biblioteca 3 — Java Testing Framework (`chaos-testing`):** Um sistema de extensão JUnit 5 com 448 anotações L1 (primitivas brutas de syscall), 92 compostas L2 (padrões de falha documentados) e 64 cenários L3 (incidentes reais de produção reproduzidos como anotações únicas). Integra-se com Spring Boot 3/4, Quarkus e Micronaut. Gerencia a injeção de LD_PRELOAD em contêineres Docker automaticamente via Docker API.
As três compartilham a mesma gramática DSL de selector × effect × probability. Você aprende o modelo mental uma vez. Aplica em todas as camadas.
Camada 1: Falhas Reais do Kernel para Qualquer Processo
O conjunto de bibliotecas C99 opera no nível mais baixo disponível para código em espaço de usuário: a fronteira libc. Quando seu processo Java chama read() em um socket, passa pelo glibc. Quando seu script Python chama socket.connect(), passa pelo glibc. É aí que interceptamos — antes do kernel, depois do seu código.
Seis objetos compartilhados independentes, cada um possuindo um conjunto disjunto de símbolos para que possam ser compostos sem conflito:
- `libchaos-io.so` — leitura/escrita de arquivo, fsync, escritas incompletas (retornos curtos), corrupção de leitura em bit único - `libchaos-net.so` — connect, send, recv, accept, com injeção de ERRNO, latência e corrupção de payload - `libchaos-dns.so` — getaddrinfo/getnameinfo, com reescrita de nomes, EAI_AGAIN, EAI_FAIL, embaralhamento e limitação de respostas - `libchaos-time.so` — clock_gettime, nanosleep com desvio de tempo com sinal e injeção de latência - `libchaos-memory.so` — mmap/munmap/mprotect com injeção de ENOMEM em probabilidade configurável - `libchaos-process.so` — fork, pthread_create, posix_spawn, exec — semântica de falha-após-N e latência
A interface é um arquivo de configuração em texto simples em um caminho bem conhecido. Sem daemons, sem APIs, sem agentes para executar. Escreva uma configuração, faça o preload da biblioteca, execute seu processo.
# /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 Camada 2: 62 Pontos de Interceptação no JDK
A camada C99 é poderosa, mas grosseira: opera em chamadas de nível de SO. Para cargas de trabalho JVM, você precisa de maior precisão. A diferença entre o HikariCP fazendo timeout no empréstimo de uma conexão versus uma conexão TCP fazendo timeout no kernel é exatamente a distinção que importa para o ajuste de circuit breakers.
O agente JVM instrumenta 62 pontos de chamada do JDK usando ByteBuddy com inlining de @Advice. Após o aquecimento do JIT, o despacho de caos compila para uma verificação de nulo e um branch não tomado — overhead próximo de zero no caso comum.
De forma crítica: cada teste recebe uma ChaosSession apoiada por ThreadLocal<UUID>. Os cenários se registram com escopo de sessão, disparando apenas nas threads que carregam o ID de sessão do teste. Múltiplos testes rodam concorrentemente no mesmo JVM sem interferência — o caos do teste A é invisível para o teste B.
Os starters do Spring Boot 3 e 4 tornam a configuração transparente — uma dependência de teste, uma anotação, e o agente se auto-anexa:
@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);
}
}
}
} Camada 3: 604 Anotações, 64 Incidentes de Produção
O framework Java é o que torna o chaos testing acessível no nível da equipe. Em vez de aprender DSLs de falha e ajustar probabilidades, um desenvolvedor anota um teste com o nome de um padrão de incidente real e o framework cuida da composição.
**448 anotações L1** oferecem controle cirúrgico no nível de syscall: `@ChaosRecvEconnreset(probability = 0.05)`, `@ChaosDnsGetaddrinfoEaiAgain(hostPattern = "*.internal", probability = 0.20)`, `@ChaosMmapEnomem(probability = 0.001)`
**92 compostas L2** agrupam falhas relacionadas em padrões documentados: `@CompositeChaosTransientDnsFailure`, `@CompositeChaosConnectionRefused`, `@CompositeChaosLowMemoryPressure`
**64 cenários L3** codificam incidentes reais de produção como anotações únicas — cada um com uma classificação de severidade, referência de origem e uma classe Composer multi-domínio que sabe como decompor o incidente na mistura certa de falhas de syscall:
`@IncidentChaosK8sRollingUpdateRst` — ECONNRESET em 30% das chamadas RECV, reproduzindo a janela de atraso do iptables durante atualizações contínuas do Kubernetes
`@IncidentChaosFeignRetryAmplification` — ECONNREFUSED em 50% do connect() mais injeção de IOException no nível do Feign, reproduzindo o padrão de tempestade de retry que cria 15x de amplificação de carga
`@IncidentChaosSpringTransactionalPoolDeadlock` — o padrão de deadlock @Transactional(REQUIRES_NEW) que esgota o pool de conexões de dentro
O framework injeta automaticamente as bibliotecas LD_PRELOAD corretas no seu contêiner Docker antes do início do teste — via fluxo tar da Docker API, sem shell exec, sem sidecar, sem modificação de imagem necessária.
@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);
}
} Uma DSL, Três Camadas, Sem Lacunas
A decisão de design fundamental é que as três camadas compartilham a mesma gramática conceitual: selector × effect × policy.
Um **selector** define o alvo da falha: um prefixo de caminho de arquivo, um endpoint TCP, um tipo de operação do JDK, um padrão de nome DNS. Um **effect** define como falhá-lo: injeção de ERRNO, latência, corrupção de dados, rejeição, corrupção de valor em limite. Uma **policy** controla quando falhar: probabilidade, limite de taxa, contagem de aquecimento, janela de tempo, máximo de aplicações.
Seja escrevendo um arquivo de configuração C99, construindo um ChaosScenario para o agente JVM ou escolhendo uma anotação JUnit 5, você trabalha com o mesmo modelo mental. O desenvolvedor que entende `@IncidentChaosFeignRetryAmplification` pode aprofundar-se nas anotações L1 subjacentes e ler exatamente quais syscalls estão sendo falhadas, em que taxas e por quê.
E as três camadas podem estar ativas simultaneamente. Uma única execução de teste pode ter: - O contêiner rodando com `libchaos-net.so` injetado (30% ECONNRESET) - O agente JVM produzindo pressão no cache de código (estressor em background) - Uma ChaosSession com rejeição JDBC ativa (os primeiros 3 empréstimos do pool falham)
As três controladas de forma independente, as três com escopo correto, nenhuma interferindo com as outras.
Caos como Cidadão de Primeira Classe no CI
A mudança que defendemos não é sobre Game Days ou experimentos de caos em produção. É mais simples e mais valiosa: **todo PR que toque em tratamento de erros, lógica de retry, circuit breakers ou gerenciamento de conexões deve ter um teste de caos que faça gate do build**.
Isso reformula o chaos engineering de uma disciplina de operações para uma disciplina de desenvolvimento. A questão não é mais "nossa plataforma consegue sobreviver a uma partição de rede?" — respondida em produção, tarde demais. A questão passa a ser "o circuit breaker deste PR abre corretamente quando o pool JDBC se esgota, antes que esse código seja entregue?"
As ferramentas existem. As anotações estão a um import de distância. A injeção no Docker é automática. O custo de adicionar `@IncidentChaosK8sRollingUpdateRst` a um teste de integração é o mesmo que adicionar qualquer outra anotação JUnit. O custo de não tê-la é o que você paga às 3 da manhã.
Por Onde Começar
Cada biblioteca tem seu próprio post de aprofundamento, mas se você quer testes de caos rodando hoje, comece com o framework Java — ele tem a história de primeiros passos mais completa e o impacto mais imediato.
**Passo 1:** Adicione a dependência para o seu framework (Spring Boot 3/4, Quarkus ou Micronaut). **Passo 2:** Anote seus testes de contêiner existentes com @ExtendWith(ChaosTestingExtension.class) e @SyscallLevelChaos. **Passo 3:** Escolha uma anotação L3 que corresponda a um modo de falha que você já viu em produção ou sobre o qual se preocupa. **Passo 4:** Execute o teste. Observe-o falhar se o seu código de resiliência ainda não está lá. Corrija-o. Entregue-o com confiança.
A camada de caos não substitui suas outras fases de teste. Ela as completa.
- chaos-testing-libraries — falhas C99 LD_PRELOAD para qualquer processo ELF, qualquer linguagem
- chaos-testing-java-agent — agente bytecode JVM, 62 pontos de chamada do JDK, isolamento de sessão por teste
- chaos-testing — framework JUnit 5, Spring Boot / Quarkus / Micronaut, 64 anotações de incidentes
Key Takeaways
A pirâmide de testes nunca esteve errada — estava incompleta. Testes unitários verificam corretude. Testes de integração verificam contratos. Testes de contêiner verificam comportamento contra dependências reais. Testes de caos verificam a sobrevivência em um ambiente hostil.
Construímos três bibliotecas que cobrem essa última milha: um toolkit C99 LD_PRELOAD para interceptação libc real do kernel, um agente bytecode JVM com 62 pontos de interceptação e isolamento de sessão por teste, e um framework de anotações JUnit 5 com 64 incidentes reais de produção codificados como cenários de teste reutilizáveis.
A Fase 4 está pronta. Seu pipeline de CI está esperando.
Engineering Team
Senior Solutions Architects
Construímos sistemas distribuídos desde antes de 'microsserviços' ser um termo. Nossas cicatrizes contam histórias.