A Quarta Fase dos Testes: Por Que o Chaos Engineering Pertence ao Seu Pipeline de CI
Engenharia category.testing 26 de junho de 2026

A Quarta Fase dos Testes: Por Que o Chaos Engineering Pertence ao Seu Pipeline de CI

Testes unitários, testes de integração, testes de contêiner — e agora testes de caos. Construímos uma pilha de chaos engineering em três camadas usando C99, Java e JUnit 5 que transforma incidentes de produção em gates de CI reproduzíveis.

E
Engineering Team
Senior Solutions Architects
12 min de leitura

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.

text
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.

bash
# /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:

java
@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.

java
@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.

#chaos-engineering #testing #ci-cd #java #c99 #ldpreload #jvm #junit5 #spring-boot #resilience
E

Engineering Team

Senior Solutions Architects

Construímos sistemas distribuídos desde antes de 'microsserviços' ser um termo. Nossas cicatrizes contam histórias.