La Quatrième Phase des Tests : Pourquoi le Chaos Engineering a sa Place dans votre Pipeline CI
Ingénierie category.testing 26 juin 2026

La Quatrième Phase des Tests : Pourquoi le Chaos Engineering a sa Place dans votre Pipeline CI

Tests unitaires, tests d'intégration, tests de conteneurs — et maintenant tests de chaos. Nous avons construit une pile de chaos engineering à trois couches en C99, Java et JUnit 5 qui transforme les incidents de production en portes CI reproductibles.

E
Engineering Team
Senior Solutions Architects
12 min de lecture

Le Test qui a Réussi. L'Incident de Production qui n'a pas Réussi.

Il est 3 h du matin. Votre service renvoie des 503. Le disjoncteur dont vous êtes convaincu qu'il fonctionne correctement — parce qu'il a des tests — ne s'ouvre pas. Votre logique de relance, elle aussi testée, amplifie le problème par 9. Votre résolveur DNS génère des EAI_AGAIN contre un cluster CoreDNS surchargé, et chaque nouvelle tentative aggrave la situation.

Vous effectuez un retour arrière. L'incident se clôture. Vous rédigez le post-mortem.

Six semaines plus tard, vous déployez à nouveau. Le même mode de défaillance, sous une forme légèrement différente. Les tests passent toujours.

Nous avons observé ce schéma des dizaines de fois. Les tests ne sont pas faux. Le comportement testé est réel. Mais il existe une catégorie de défaillance qu'aucun test unitaire, test d'intégration ou test de conteneur ne pourra jamais détecter : ce qui se passe quand l'infrastructure sous-jacente à votre application commence à se comporter de manière techniquement valide mais opérationnellement hostile. Que se passe-t-il quand le DNS commence à osciller ? Quand les écritures réseau retournent des valeurs courtes ? Quand votre pool de connexions se vide à zéro, non pas à cause d'un bug, mais à cause d'une charge coordonnée ?

C'est le fossé. Et c'est pourquoi nous avons construit une pile de tests de chaos.

Les Quatre Phases des Tests Modernes

La plupart des équipes d'ingénierie pensent aux tests en trois phases. Nous soutenons qu'il en existe quatre, et que sauter la quatrième est ce qui rend possible les incidents nocturnes.

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

Ce que les Trois Premières Phases ne Peuvent pas Tester

Soyons précis sur ce fossé. Voici ce que les phases 1 à 3 ne peuvent pas valider :

**Votre disjoncteur s'ouvre réellement.** Vous pouvez tester unitairement la configuration de Resilience4j, mais le disjoncteur ne s'ouvrira pas à moins que l'appel en aval ne commence réellement à échouer au bon taux, au bon moment, avec le bon type d'exception. Les mocks rendent l'échec trop propre.

**Votre logique de relance n'amplifie pas l'échec.** Une relance 5x sur trois répliques contre un backend défaillant n'est pas une amplification de 5x — c'est 15x. C'est invisible dans les tests unitaires. C'est invisible dans les tests d'intégration à moins d'ingénier soigneusement des défaillances partielles.

**Votre client DNS gère les échecs de résolution transitoires.** getaddrinfo() retournant EAI_AGAIN est une réponse POSIX valide et attendue. La plupart des services ne testent jamais ce chemin. Résultat : un problème passager de CoreDNS se transforme en une coupure de 30 secondes.

**Votre pool de connexions récupère sous pression.** L'épuisement du pool suivi d'une récupération partielle est une machine à états nuancée. La logique de récupération est généralement écrite une fois, jamais testée, et découverte en production.

**Votre pile de délais d'attente se compose correctement.** Un délai de lecture de 30 s derrière un délai de client HTTP de 10 s derrière un délai d'équilibreur de charge de 5 s — le comportement composé n'est pas ce qu'un test individuel quelconque exerce.

Présentation de la Pile de Tests de Chaos

Nous avons construit trois bibliothèques open source qui couvrent ensemble l'intégralité de la surface des tests de chaos de phase 4.

**Bibliothèque 1 — Chaos C99 LD_PRELOAD (`chaos-testing-libraries`) :** Un ensemble d'objets partagés qui interceptent les appels libc au niveau de l'éditeur de liens dynamique. Indépendant du langage — fonctionne sur tout binaire ELF : Java, Go, Python, Node.js, Rust avec CGO. Six domaines de défauts : E/S de fichiers, réseau, DNS, horloge, mémoire et cycle de vie des processus. Aucune modification du code applicatif requise. L'interface entière est un fichier de configuration en texte brut.

**Bibliothèque 2 — Agent de Chaos JVM (`chaos-testing-java-agent`) :** Un agent Java qui instrumente 62 points d'appel du JDK en utilisant ByteBuddy. Injecte des défaillances, des délais et une pression sur les ressources au niveau de la JVM — à l'intérieur de HikariCP, de Netty, des piles SSL et DNS natives du JDK. Isolé par session par test, de sorte que plusieurs tests de chaos peuvent s'exécuter simultanément dans la même JVM sans interférer les uns avec les autres.

**Bibliothèque 3 — Framework de Test Java (`chaos-testing`) :** Un système d'extension JUnit 5 avec 448 annotations L1 (primitives d'appel système brutes), 92 composites L2 (schémas de défaillance documentés) et 64 scénarios L3 (vrais incidents de production reproduits sous forme d'annotations uniques). S'intègre avec Spring Boot 3/4, Quarkus et Micronaut. Gère automatiquement l'injection LD_PRELOAD dans les conteneurs Docker via l'API Docker.

Les trois partagent la même grammaire DSL sélecteur × effet × probabilité. Vous apprenez le modèle mental une seule fois. Vous l'appliquez à chaque couche.

Couche 1 : Défauts Réels au Niveau du Noyau pour Tout Processus

La bibliothèque C99 opère au niveau le plus bas disponible pour le code en espace utilisateur : la frontière libc. Lorsque votre processus Java appelle read() sur un socket, cela passe par glibc. Lorsque votre script Python appelle socket.connect(), cela passe par glibc. C'est là que nous interceptons — avant le noyau, après votre code.

Six objets partagés indépendants, chacun possédant un ensemble disjoint de symboles afin de pouvoir être composés sans conflit :

- `libchaos-io.so` — lecture/écriture de fichiers, fsync, écritures tronquées (retours courts), corruption de lecture d'un seul bit - `libchaos-net.so` — connect, send, recv, accept, avec injection d'ERRNO, latence et corruption de charge utile - `libchaos-dns.so` — getaddrinfo/getnameinfo, avec réécriture de noms, EAI_AGAIN, EAI_FAIL, mélange et limitation des réponses - `libchaos-time.so` — clock_gettime, nanosleep avec décalage temporel signé et injection de latence - `libchaos-memory.so` — mmap/munmap/mprotect avec injection d'ENOMEM à une probabilité configurable - `libchaos-process.so` — fork, pthread_create, posix_spawn, exec — sémantique d'échec-après-N et latence

L'interface est un fichier de configuration en texte brut à un chemin bien connu. Pas de démons, pas d'API, pas d'agents à exécuter. Écrivez une configuration, préchargez la bibliothèque, exécutez votre processus.

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

Couche 2 : 62 Points d'Interception JDK

La couche C99 est puissante mais grossière : elle opère sur des appels au niveau du système d'exploitation. Pour les charges de travail JVM, vous avez besoin d'une précision plus fine. La différence entre HikariCP mettant en timeout l'emprunt d'une connexion versus un timeout de connexion TCP au niveau du noyau est exactement la distinction qui importe pour le réglage des disjoncteurs.

L'agent JVM instrumente 62 points d'appel JDK en utilisant ByteBuddy avec l'inlining @Advice. Après la mise en chauffe du JIT, la distribution du chaos se compile jusqu'à une vérification de null et une branche non prise — surcharge quasi nulle dans le cas courant.

Fait essentiel : chaque test obtient une ChaosSession adossée à un ThreadLocal<UUID>. Les scénarios s'enregistrent avec une portée de session, ne se déclenchant que sur les threads qui portent l'ID de session du test. Plusieurs tests s'exécutent simultanément dans la même JVM sans interférence — le chaos du test A est invisible pour le test B.

Les starters Spring Boot 3 et 4 rendent le câblage transparent — une seule dépendance de test, une seule annotation, et l'agent s'attache de lui-même :

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

Couche 3 : 604 Annotations, 64 Incidents de Production

Le framework Java est ce qui rend les tests de chaos accessibles au niveau de l'équipe. Au lieu d'apprendre des DSL de défauts et d'ajuster des probabilités, un développeur annote un test avec le nom d'un schéma d'incident réel et le framework se charge de la composition.

**448 annotations L1** offrent un contrôle chirurgical au niveau de l'appel système : `@ChaosRecvEconnreset(probability = 0.05)`, `@ChaosDnsGetaddrinfoEaiAgain(hostPattern = "*.internal", probability = 0.20)`, `@ChaosMmapEnomem(probability = 0.001)`

**92 composites L2** regroupent des défauts liés en schémas documentés : `@CompositeChaosTransientDnsFailure`, `@CompositeChaosConnectionRefused`, `@CompositeChaosLowMemoryPressure`

**64 scénarios L3** encodent de vrais incidents de production sous forme d'annotations uniques — chacune avec une cote de sévérité, une référence source, et une classe Composer multi-domaine qui sait comment décomposer l'incident en un bon mélange de défauts d'appel système :

`@IncidentChaosK8sRollingUpdateRst` — ECONNRESET sur 30 % des appels RECV, reproduisant la fenêtre de décalage iptables lors des mises à jour progressives Kubernetes

`@IncidentChaosFeignRetryAmplification` — ECONNREFUSED sur 50 % des appels connect() plus injection d'IOException au niveau Feign, reproduisant le schéma de tempête de relances qui crée une amplification de charge de 9x

`@IncidentChaosSpringTransactionalPoolDeadlock` — le schéma de blocage @Transactional(REQUIRES_NEW) qui draine le pool de connexions de l'intérieur

Le framework injecte automatiquement les bonnes bibliothèques LD_PRELOAD dans votre conteneur Docker avant le démarrage du test — via le flux tar de l'API Docker, sans exec shell, sans sidecar, sans modification d'image requise.

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, Trois Couches, Aucun Angle Mort

La décision de conception clé est que les trois couches partagent la même grammaire conceptuelle : sélecteur × effet × politique.

Un **sélecteur** cible ce qu'il faut perturber : un préfixe de chemin de fichier, un point de terminaison TCP, un type d'opération JDK, un schéma de nom DNS. Un **effet** définit comment le perturber : injection d'ERRNO, latence, corruption de données, rejet, corruption de valeur limite. Une **politique** contrôle quand le perturber : probabilité, limite de débit, compteur de mise en chauffe, fenêtre temporelle, nombre maximum d'applications.

Que vous écriviez un fichier de configuration C99, construisiez un ChaosScenario pour l'agent JVM, ou choisissiez une annotation JUnit 5, vous travaillez avec le même modèle mental. Le développeur qui comprend `@IncidentChaosFeignRetryAmplification` peut explorer les annotations L1 sous-jacentes et lire exactement quels appels système sont perturbés, à quels taux, et pourquoi.

Et les trois couches peuvent être actives simultanément. Une seule exécution de test peut avoir : - Le conteneur s'exécutant avec `libchaos-net.so` injecté (30 % ECONNRESET) - L'agent JVM produisant une pression sur le cache de code (stresseur en arrière-plan) - Une ChaosSession avec rejet JDBC actif (les 3 premiers emprunts du pool échouent)

Les trois sont contrôlées indépendamment, toutes trois correctement délimitées, aucune n'interfère avec les autres.

Le Chaos comme Citoyen de Première Classe du CI

Le changement que nous préconisons ne concerne pas les Game Days ou les expériences de chaos en production. C'est plus simple et plus précieux : **chaque PR qui touche à la gestion des erreurs, à la logique de relance, aux disjoncteurs ou à la gestion des connexions devrait avoir un test de chaos qui bloque le build**.

Cela recadre le chaos engineering d'une discipline ops vers une discipline de développement. La question n'est plus « notre plateforme peut-elle survivre à une partition réseau ? » — réponse obtenue en production, trop tard. La question devient « le disjoncteur de cette PR s'ouvre-t-il correctement quand le pool JDBC s'épuise, avant que ce code ne soit mis en production ? »

L'outillage existe. Les annotations ne sont qu'un import de distance. L'injection Docker est automatique. Le surcoût d'ajout de `@IncidentChaosK8sRollingUpdateRst` à un test d'intégration est le même que celui d'ajout de n'importe quelle autre annotation JUnit. Le surcoût de ne pas l'avoir, c'est ce que vous payez à 3 h du matin.

Par Où Commencer

Chaque bibliothèque a son propre article approfondi, mais si vous voulez que des tests de chaos s'exécutent dès aujourd'hui, commencez par le framework Java — il a le guide de démarrage le plus complet et l'impact le plus immédiat.

**Étape 1 :** Ajoutez la dépendance pour votre framework (Spring Boot 3/4, Quarkus ou Micronaut). **Étape 2 :** Annotez vos tests de conteneurs existants avec @ExtendWith(ChaosTestingExtension.class) et @SyscallLevelChaos. **Étape 3 :** Choisissez une annotation L3 qui correspond à un mode de défaillance que vous avez observé en production ou qui vous préoccupe. **Étape 4 :** Exécutez le test. Regardez-le échouer si votre code de résilience n'est pas encore en place. Corrigez-le. Livrez-le avec confiance.

La couche chaos ne remplace pas vos autres phases de test. Elle les complète.

  • chaos-testing-libraries — défauts C99 LD_PRELOAD pour tout processus ELF, tout langage
  • chaos-testing-java-agent — agent bytecode JVM, 62 points d'appel JDK, isolation de session par test
  • chaos-testing — framework JUnit 5, Spring Boot / Quarkus / Micronaut, 64 annotations d'incidents

Key Takeaways

La pyramide des tests n'a jamais été fausse — elle était incomplète. Les tests unitaires vérifient la correction. Les tests d'intégration vérifient les contrats. Les tests de conteneurs vérifient le comportement face aux vraies dépendances. Les tests de chaos vérifient la survie dans un environnement hostile.

Nous avons construit trois bibliothèques qui couvrent ce dernier kilomètre : une boîte à outils C99 LD_PRELOAD pour l'interception libc réelle au niveau du noyau, un agent bytecode JVM avec 62 points d'interception et une isolation de session par test, et un framework d'annotations JUnit 5 avec 64 vrais incidents de production encodés comme scénarios de test réutilisables.

La phase 4 est prête. Votre pipeline CI vous attend.

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

Engineering Team

Senior Solutions Architects

Nous construisons des systèmes distribués depuis avant que le terme « microservices » n'existe. Nos cicatrices racontent des histoires.