La Quarta Fase del Testing: Perché il Chaos Engineering Appartiene alla Tua Pipeline CI
Ingegneria category.testing 26 giugno 2026

La Quarta Fase del Testing: Perché il Chaos Engineering Appartiene alla Tua Pipeline CI

Unit test, integration test, container test — e ora chaos test. Abbiamo costruito uno stack di chaos engineering a tre livelli in C99, Java e JUnit 5 che trasforma gli incidenti in produzione in gate CI riproducibili.

E
Engineering Team
Senior Solutions Architects
12 min di lettura

Il Test Che Ha Passato. L'Incidente in Produzione Che Non l'Ha Fatto.

Sono le 3 di notte. Il tuo servizio sta restituendo errori 503. Il circuit breaker di cui sei convinto funzioni correttamente — perché ha i test — non si sta aprendo. La tua logica di retry, anch'essa testata, sta amplificando il problema di 9 volte. Il tuo resolver DNS continua a ricevere EAI_AGAIN da un cluster CoreDNS sovraccarico, e ogni retry peggiora la situazione.

Fai il rollback. L'incidente si chiude. Scrivi il post-mortem.

Sei settimane dopo, esegui nuovamente il deploy. La stessa modalità di fallimento, con una forma leggermente diversa. I test passano ancora.

Abbiamo visto questo schema decine di volte. I test non sono sbagliati. Il comportamento testato è reale. Ma esiste una categoria di fallimento che nessuna quantità di unit test, integration test o container test coglierà mai: cosa succede quando l'infrastruttura sotto la tua applicazione inizia a comportarsi in modi tecnicamente validi ma operativamente ostili. Cosa succede quando il DNS inizia a flappare? Quando le scritture di rete restituiscono meno dati del previsto? Quando il tuo connection pool si svuota a zero, non per un bug, ma a causa di un carico coordinato?

Questo è il divario. Ed è per questo che abbiamo costruito uno stack di chaos testing.

Le Quattro Fasi del Testing Moderno

La maggior parte dei team di ingegneria pensa al testing in tre fasi. Noi sosteniamo che siano quattro, e che saltare la quarta è ciò che rende possibili gli incidenti notturni.

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

Cosa Le Prime Tre Fasi Non Possono Testare

Siamo specifici riguardo al divario. Ecco cosa le Fasi 1–3 non possono validare:

**Il tuo circuit breaker si apre davvero.** Puoi eseguire uno unit test sulla configurazione di Resilience4j, ma il circuit breaker non si aprirà a meno che la chiamata downstream non fallisca effettivamente alla giusta frequenza, con la giusta tempistica, con il giusto tipo di eccezione. I mock rendono il fallimento troppo pulito.

**La tua logica di retry non amplifica il fallimento.** Un retry 5x su tre repliche contro un backend che fallisce non è un'amplificazione 5x — è 15x. Questo è invisibile negli unit test. È invisibile negli integration test a meno che non ingegnerizzi attentamente i fallimenti parziali.

**Il tuo client DNS gestisce i fallimenti di risoluzione transitori.** getaddrinfo() che restituisce EAI_AGAIN è una risposta POSIX valida e attesa. La maggior parte dei servizi non testa mai questo percorso. Il risultato: un'interruzione di CoreDNS si propaga a cascata in un brownout di 30 secondi.

**Il tuo connection pool si recupera sotto pressione.** L'esaurimento del pool seguito da un recupero parziale è una macchina a stati sfumata. La logica di recupero viene solitamente scritta una volta, mai testata, e scoperta in produzione.

**Il tuo stack di timeout si compone correttamente.** Un read timeout di 30s dietro un timeout del client HTTP di 10s dietro un timeout del load balancer di 5s — il comportamento composto non è quello che nessun singolo test esercita.

Presentazione dello Stack di Chaos Testing

Abbiamo costruito tre librerie open-source che insieme coprono l'intera superficie del chaos testing di Fase 4.

**Libreria 1 — C99 LD_PRELOAD Chaos (`chaos-testing-libraries`):** Un insieme di shared object che intercettano le chiamate libc a livello del linker dinamico. Indipendente dal linguaggio — funziona su qualsiasi binario ELF: Java, Go, Python, Node.js, Rust con CGO. Sei domini di fault: I/O su file, networking, DNS, clock, memoria e ciclo di vita del processo. Zero modifiche al codice applicativo richieste. L'intera interfaccia è un file di configurazione in testo semplice.

**Libreria 2 — JVM Chaos Agent (`chaos-testing-java-agent`):** Un agente Java che strumenta 62 call site del JDK usando ByteBuddy. Inietta fallimenti, ritardi e pressione sulle risorse a livello JVM — dentro HikariCP, dentro Netty, dentro i propri stack SSL e DNS del JDK. Isolato per sessione per ogni test, così più chaos test possono girare in concorrenza nella stessa JVM senza interferire tra loro.

**Libreria 3 — Java Testing Framework (`chaos-testing`):** Un sistema di estensioni JUnit 5 con 448 annotazioni L1 (primitive syscall grezze), 92 compositi L2 (pattern di fallimento documentati) e 64 scenari L3 (incidenti reali in produzione riprodotti come singole annotazioni). Si integra con Spring Boot 3/4, Quarkus e Micronaut. Gestisce automaticamente l'iniezione di LD_PRELOAD nei container Docker tramite le Docker API.

Tutte e tre condividono la stessa grammatica DSL di tipo selector × effect × probability. Si impara il modello mentale una volta. Lo si applica a ogni livello.

Livello 1: Fault Reali del Kernel per Qualsiasi Processo

La libreria C99 opera al livello più basso disponibile per il codice user-space: il confine libc. Quando il tuo processo Java chiama read() su un socket, passa attraverso glibc. Quando il tuo script Python chiama socket.connect(), passa attraverso glibc. È lì che intercettiamo — prima del kernel, dopo il tuo codice.

Sei shared object indipendenti, ognuno che possiede un insieme disgiunto di simboli così possono essere composti senza conflitti:

- `libchaos-io.so` — lettura/scrittura su file, fsync, scritture troncate (short return), corruzione di singoli bit in lettura - `libchaos-net.so` — connect, send, recv, accept, con iniezione di ERRNO, latenza e corruzione del payload - `libchaos-dns.so` — getaddrinfo/getnameinfo, con riscritture dei nomi, EAI_AGAIN, EAI_FAIL, mescolamento e limitazione delle risposte - `libchaos-time.so` — clock_gettime, nanosleep con skew temporale con segno e iniezione di latenza - `libchaos-memory.so` — mmap/munmap/mprotect con iniezione di ENOMEM a probabilità configurabile - `libchaos-process.so` — fork, pthread_create, posix_spawn, exec — semantica fail-after-N e latenza

L'interfaccia è un file di configurazione in testo semplice in un percorso noto. Nessun daemon, nessuna API, nessun agente da eseguire. Scrivi una configurazione, precarica la libreria, esegui il tuo 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

Livello 2: 62 Punti di Intercettazione del JDK

Il livello C99 è potente ma grossolano: opera su chiamate a livello di sistema operativo. Per i carichi di lavoro JVM, è necessaria una precisione maggiore. La differenza tra HikariCP che va in timeout sul borrow di una connessione rispetto a una connessione TCP che va in timeout a livello kernel è esattamente la distinzione che conta per la taratura dei circuit breaker.

L'agente JVM strumenta 62 call site del JDK usando ByteBuddy con inlining @Advice. Dopo il warm-up del JIT, il chaos dispatch viene compilato in un controllo null e un branch non preso — overhead quasi nullo nel caso comune.

In modo fondamentale: ogni test riceve una ChaosSession supportata da ThreadLocal<UUID>. Gli scenari si registrano come con scope di sessione, attivandosi solo sui thread che portano il session ID del test. Più test girano in concorrenza nella stessa JVM senza interferenze — il chaos del test A è invisibile al test B.

I starter per Spring Boot 3 e 4 rendono il cablaggio trasparente — una dipendenza di test, un'annotazione, e l'agente si auto-allega:

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);
            }
        }
    }
}

Livello 3: 604 Annotazioni, 64 Incidenti in Produzione

Il framework Java è ciò che rende il chaos testing accessibile a livello di team. Invece di imparare DSL di fault e affinare le probabilità, uno sviluppatore annota un test con il nome di un pattern di incidente reale e il framework gestisce la composizione.

**448 annotazioni L1** forniscono controllo chirurgico a livello di syscall: `@ChaosRecvEconnreset(probability = 0.05)`, `@ChaosDnsGetaddrinfoEaiAgain(hostPattern = "*.internal", probability = 0.20)`, `@ChaosMmapEnomem(probability = 0.001)`

**92 compositi L2** raggruppano fault correlati in pattern documentati: `@CompositeChaosTransientDnsFailure`, `@CompositeChaosConnectionRefused`, `@CompositeChaosLowMemoryPressure`

**64 scenari L3** codificano incidenti reali in produzione come singole annotazioni — ognuna con un rating di severità, un riferimento alla fonte e una classe Composer multi-dominio che sa come scomporre l'incidente nel giusto mix di fault a livello di syscall:

`@IncidentChaosK8sRollingUpdateRst` — ECONNRESET sul 30% delle chiamate RECV, riproducendo la finestra di lag di iptables durante i rolling update di Kubernetes

`@IncidentChaosFeignRetryAmplification` — ECONNREFUSED sul 50% delle chiamate connect() più iniezione di IOException a livello Feign, riproducendo il pattern di retry storm che crea un'amplificazione del carico di 9x

`@IncidentChaosSpringTransactionalPoolDeadlock` — il pattern di deadlock @Transactional(REQUIRES_NEW) che svuota il connection pool dall'interno

Il framework inietta automaticamente le giuste librerie LD_PRELOAD nel tuo container Docker prima dell'avvio del test — tramite il tar stream delle Docker API, senza exec di shell, senza sidecar, senza modifiche all'immagine richieste.

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);
    }
}

Un DSL, Tre Livelli, Nessun Divario

La decisione progettuale chiave è che tutti e tre i livelli condividono la stessa grammatica concettuale: selector × effect × policy.

Un **selector** punta a cosa mettere in fault: un prefisso di percorso file, un endpoint TCP, un tipo di operazione JDK, un pattern di nome DNS. Un **effect** definisce come metterlo in fault: iniezione di ERRNO, latenza, corruzione dei dati, rifiuto, corruzione dei valori limite. Una **policy** controlla quando mettere in fault: probabilità, rate limit, conteggio di warm-up, finestra temporale, numero massimo di applicazioni.

Che tu stia scrivendo un file di configurazione C99, costruendo un ChaosScenario per l'agente JVM o scegliendo un'annotazione JUnit 5, stai lavorando con lo stesso modello mentale. Lo sviluppatore che capisce `@IncidentChaosFeignRetryAmplification` può addentrarsi nelle annotazioni L1 sottostanti e leggere esattamente quali syscall vengono messe in fault, a quali frequenze, e perché.

E tutti e tre i livelli possono essere attivi contemporaneamente. Una singola esecuzione di test può avere: - Il container in esecuzione con `libchaos-net.so` iniettato (30% ECONNRESET) - L'agente JVM che produce pressione sulla code cache (stressor in background) - Una ChaosSession con rifiuto JDBC attivo (i primi 3 borrow del pool falliscono)

Tutti e tre controllati indipendentemente, tutti e tre con scope corretto, nessuno che interferisce con gli altri.

Il Chaos Come Cittadino di Prima Classe nella CI

Il cambiamento che stiamo sostenendo non riguarda i Game Day o gli esperimenti di chaos in produzione. È più semplice e più prezioso: **ogni PR che tocca la gestione degli errori, la logica di retry, i circuit breaker o la gestione delle connessioni dovrebbe avere un chaos test che blocca la build**.

Questo riconfigura il chaos engineering da disciplina operativa a disciplina di sviluppo. La domanda non è più "la nostra piattaforma sopravvive a una partizione di rete?" — risposta in produzione, troppo tardi. La domanda diventa "il circuit breaker di questa PR si apre correttamente quando il pool JDBC si esaurisce, prima che questo codice venga rilasciato?"

Gli strumenti esistono. Le annotazioni distano un import. L'iniezione Docker è automatica. Il costo di aggiungere `@IncidentChaosK8sRollingUpdateRst` a un integration test è lo stesso di aggiungere qualsiasi altra annotazione JUnit. Il costo di non averla è quello che paghi alle 3 di notte.

Da Dove Iniziare

Ogni libreria ha il suo post di approfondimento, ma se vuoi che i chaos test girino oggi, inizia con il framework Java — ha la storia di getting-started più completa e l'impatto più immediato.

**Passo 1:** Aggiungi la dipendenza per il tuo framework (Spring Boot 3/4, Quarkus o Micronaut). **Passo 2:** Annota i tuoi container test esistenti con @ExtendWith(ChaosTestingExtension.class) e @SyscallLevelChaos. **Passo 3:** Scegli un'annotazione L3 che corrisponde a una modalità di fallimento che hai visto in produzione o che ti preoccupa. **Passo 4:** Esegui il test. Osservalo fallire se il tuo codice di resilienza non è ancora presente. Correggilo. Rilascialo con fiducia.

Il livello chaos non sostituisce le altre fasi di test. Le completa.

  • chaos-testing-libraries — C99 LD_PRELOAD fault per qualsiasi processo ELF, qualsiasi linguaggio
  • chaos-testing-java-agent — agente bytecode JVM, 62 call site del JDK, isolamento per sessione per ogni test
  • chaos-testing — framework JUnit 5, Spring Boot / Quarkus / Micronaut, 64 annotazioni di incidenti

Key Takeaways

La piramide del testing non è mai stata sbagliata — era incompleta. Gli unit test verificano la correttezza. Gli integration test verificano i contratti. I container test verificano il comportamento contro dipendenze reali. I chaos test verificano la sopravvivenza in un ambiente ostile.

Abbiamo costruito tre librerie che coprono quest'ultimo miglio: un toolkit C99 LD_PRELOAD per l'intercettazione libc reale a livello kernel, un agente bytecode JVM con 62 punti di intercettazione e isolamento per sessione per ogni test, e un framework di annotazioni JUnit 5 con 64 incidenti reali in produzione codificati come scenari di test riutilizzabili.

La Fase 4 è pronta. La tua pipeline CI sta aspettando.

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

Engineering Team

Senior Solutions Architects

Costruiamo sistemi distribuiti da prima che 'microservizi' fosse una parola di moda. Le nostre cicatrici raccontano storie.