[{"data":1,"prerenderedAt":2017},["ShallowReactive",2],{"blog-post-\u002Fblogs\u002F2026-06-23-le-vert-ne-suffit-pas-3-facons-de-douter-de-son-code":3,"related-\u002Fblogs\u002F2026-06-23-le-vert-ne-suffit-pas-3-facons-de-douter-de-son-code":2016},{"id":4,"title":5,"alt":6,"authors":7,"body":8,"date":1966,"description":1967,"extension":1968,"image":1969,"meta":1970,"navigation":137,"ogImage":1990,"path":2009,"published":2010,"reviewers":2011,"seo":2012,"stem":2013,"tags":2014,"__hash__":2015},"blogs\u002Fblogs\u002F2026-06-23-le-vert-ne-suffit-pas-3-facons-de-douter-de-son-code\u002Findex.md","Le vert ne suffit pas : 3 façons de douter de son code","Illustration de couverture pour l'article \"Le vert ne suffit pas\". Au centre, un grand badge vert fissuré affichant \"ALL TESTS GREEN : Coverage 98%\" avec une coche, entouré de flammes et de fumée : symbolisant une fausse confiance.",[],{"type":9,"value":10,"toc":1956},"minimark",[11,18,21,33,36,39,44,67,70,209,223,248,255,270,278,317,356,359,366,369,393,399,404,407,410,413,417,420,432,445,521,548,567,680,691,694,714,730,732,736,742,773,815,825,841,922,933,951,1054,1069,1084,1091,1257,1268,1279,1281,1285,1296,1315,1321,1350,1384,1491,1503,1517,1626,1641,1665,1741,1764,1773,1791,1802,1804,1808,1815,1826,1828,1832,1837,1865,1870,1899,1904,1929,1934,1953],[12,13,14],"p",{},[15,16,17],"em",{},"Retour de DevLille 2026, côté Java\u002FKotlin\u002FSpringboot.",[12,19,20],{},"BUILD SUCCESS. Coverage à 98 %. Toute la suite de tests au vert. On connaît tous ce petit shot de dopamine du pipeline tout propre. Et on connaît aussi le bug qui passe en prod le lendemain, l'air de rien.",[12,22,23,24,28,29,32],{},"À DevLille 2026, trois talks m'ont posé la même question dérangeante, chacun sous un angle différent : ",[25,26,27],"strong",{},"et si tout ce vert nous mentait ?"," Mon coverage ",[25,30,31],{},"qui me rassure à tort",", mon compilateur qui me laisse passer des erreurs qu'il aurait pu voir, mon appli qui fonctionne bien… tant que l'infra ne tremble pas.",[12,34,35],{},"Je travaille aujourd'hui avec Kotlin \u002F Springboot \u002F Kotest, et la plupart de ces talks étaient présentés côté Java. Le but de cet article, c'est donc autant un récap qu'un REX : qu'est-ce que ces idées donnent une fois ramenées dans une stack Kotlin ? Spoiler : parfois le langage fait le boulot à notre place, parfois pas du tout.",[37,38],"hr",{},[40,41,43],"h2",{"id":42},"_1-mes-tests-passent-mais-testent-ils-vraiment-quelque-chose","1. « Mes tests passent », mais testent-ils vraiment quelque chose ?",[12,45,46,47,50,51,54,55,58,59,62,63,66],{},"On a toutes et tous appris à regarder le ",[25,48,49],{},"code coverage",". Le piège, c'est ce qu'il mesure vraiment : le ",[15,52,53],{},"line coverage"," dit quelles lignes ont été ",[25,56,57],{},"exécutées",", le ",[15,60,61],{},"branch coverage"," quels chemins logiques ont été empruntés. Aucun des deux ne dit si tes tests ",[25,64,65],{},"vérifient"," réellement quelque chose.",[12,68,69],{},"La démonstration est cruelle. Prends ce bout de code et son test :",[71,72,77],"pre",{"className":73,"code":74,"language":75,"meta":76,"style":76},"language-kotlin shiki shiki-themes github-dark-default","fun applyDiscount(price: Double, rate: Double): Double =\n    price - (price * rate)\n\nclass DiscountTest : FunSpec({\n    test(\"applique une remise\") {\n        applyDiscount(100.0, 0.2) shouldBeGreaterThan 0.0\n    }\n})\n","kotlin","",[78,79,80,114,132,139,157,173,197,203],"code",{"__ignoreMap":76},[81,82,85,89,93,97,101,104,106,109,111],"span",{"class":83,"line":84},"line",1,[81,86,88],{"class":87},"suJrU","fun",[81,90,92],{"class":91},"sc3cj"," applyDiscount",[81,94,96],{"class":95},"sZEs4","(price: ",[81,98,100],{"class":99},"sQhOw","Double",[81,102,103],{"class":95},", rate: ",[81,105,100],{"class":99},[81,107,108],{"class":95},"): ",[81,110,100],{"class":99},[81,112,113],{"class":87}," =\n",[81,115,117,120,123,126,129],{"class":83,"line":116},2,[81,118,119],{"class":95},"    price ",[81,121,122],{"class":87},"-",[81,124,125],{"class":95}," (price ",[81,127,128],{"class":87},"*",[81,130,131],{"class":95}," rate)\n",[81,133,135],{"class":83,"line":134},3,[81,136,138],{"emptyLinePlaceholder":137},true,"\n",[81,140,142,145,148,151,154],{"class":83,"line":141},4,[81,143,144],{"class":87},"class",[81,146,147],{"class":99}," DiscountTest",[81,149,150],{"class":95}," : ",[81,152,153],{"class":99},"FunSpec",[81,155,156],{"class":95},"({\n",[81,158,160,163,166,170],{"class":83,"line":159},5,[81,161,162],{"class":91},"    test",[81,164,165],{"class":95},"(",[81,167,169],{"class":168},"s9uIt","\"applique une remise\"",[81,171,172],{"class":95},") {\n",[81,174,176,179,181,185,188,191,194],{"class":83,"line":175},6,[81,177,178],{"class":91},"        applyDiscount",[81,180,165],{"class":95},[81,182,184],{"class":183},"sFSAA","100.0",[81,186,187],{"class":95},", ",[81,189,190],{"class":183},"0.2",[81,192,193],{"class":95},") shouldBeGreaterThan ",[81,195,196],{"class":183},"0.0\n",[81,198,200],{"class":83,"line":199},7,[81,201,202],{"class":95},"    }\n",[81,204,206],{"class":83,"line":205},8,[81,207,208],{"class":95},"})\n",[12,210,211,212,214,215,218,219,222],{},"Coverage : 100 %. Tout est vert. Sauf que cette assertion ne teste presque rien : remplace le ",[78,213,122],{}," par un ",[78,216,217],{},"+"," dans la fonction, et ",[78,220,221],{},"120.0 > 0.0"," reste vrai. Le test ne le verra jamais.",[12,224,225,226,235,236,239,240,243,244,247],{},"C'était tout le sujet du talk de ",[227,228,232],"a",{"href":229,"rel":230},"https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fvictoire-ladreit-de-lacharri%C3%A8re\u002F",[231],"nofollow",[25,233,234],{},"Victoire de Lacharrière",", « Testez vos tests avant qu'ils ne vous trahissent », et de la technique qu'elle y présentait : le ",[25,237,238],{},"mutation testing",". L'outil introduit volontairement des bugs, les ",[15,241,242],{},"mutants"," (inverser un opérateur, remplacer une condition, supprimer un appel), puis relance ta suite. Si un test échoue, le mutant est ",[25,245,246],{},"tué",". S'il survit, c'est qu'aucun de tes tests ne couvrait vraiment ce comportement.",[12,249,250],{},[251,252],"img",{"alt":253,"src":254},"Illustration en deux parties expliquant le mutation testing. À gauche, \"Mutant survived\" : un bloc de code dark mode montre la version originale price - (price * rate) et la version mutante price + (price * rate), avec un badge ✅ PASS en dessous et un petit monstre vert souriant tenant une pancarte \"OK\". Légende : \"Mutant survivant : ton test ne détecte rien\". À droite, \"Mutant killed\" : le même bloc de code, avec un badge ❌ FAIL et le même monstre vert allongé mort avec des yeux en croix, à côté d'une pierre tombale. Légende : \"Mutant tué : ton test a détecté le problème\".","\u002Fcontent-assets\u002F2026-06-23-le-vert-ne-suffit-pas-3-facons-de-douter-de-son-code\u002Fassets\u002Fimg1.webp",[12,256,257,258,261,262,265,266,269],{},"La métrique qui en sort, le ",[25,259,260],{},"mutation score"," (",[78,263,264],{},"mutants tués \u002F total des mutants","), est bien plus honnête qu'un pourcentage de lignes. Le talk insistait aussi sur la ",[25,267,268],{},"test strength",", une variante qui ignore le code non couvert pour ne juger que la qualité des tests existants.",[12,271,272,273,277],{},"Un mutant survivant n'est pas toujours un oubli de test : c'est parfois le symptôme d'un test écrit sans avoir vu le rouge, ce que le ",[227,274,276],{"href":275},"\u002Fp\u002F19ff4462cd388034b74eecb49e31630d","TDD"," aurait évité dès le départ.",[12,279,280,281,288,289,294,295,300,301,304,305,308,309,312,313,316],{},"Côté outillage, chaque écosystème a le sien : ",[227,282,285],{"href":283,"rel":284},"https:\u002F\u002Fpitest.org\u002F",[231],[25,286,287],{},"pitest"," sur la JVM, ",[227,290,293],{"href":291,"rel":292},"https:\u002F\u002Fstryker-mutator.io\u002F",[231],"Stryker"," en JS\u002F.NET, ",[227,296,299],{"href":297,"rel":298},"https:\u002F\u002Fgithub.com\u002Fmutpy\u002Fmutpy",[231],"mutpy"," en Python. Et c'est là que mon approche sur Kotlin commence : pitest tourne sur le ",[25,302,303],{},"bytecode",", donc il s'applique à du Kotlin et à mes tests Kotest sans rien changer. Avec une nuance à connaître : le compilateur Kotlin génère pas mal de code synthétique (",[15,306,307],{},"null-checks",", branches de ",[78,310,311],{},"when",", valeurs par défaut) qui produit des ",[25,314,315],{},"mutants équivalents"," : des mutations qui ne changent rien d'observable, que tes tests ne peuvent donc pas tuer. Sur un projet Kotlin, ce bruit n'est pas anecdotique, et c'est là que le choix de l'outillage se complique.",[12,318,319,320,327,328,331,332,339,340,347,348,351,352,355],{},"Le plugin historique ",[227,321,324],{"href":322,"rel":323},"https:\u002F\u002Fgithub.com\u002Fpitest\u002Fpitest-kotlin",[231],[78,325,326],{},"pitest-kotlin",", gratuit et open source, n'est plus maintenu : son dépôt est archivé et renvoie explicitement vers l'offre commerciale d'",[25,329,330],{},"arcmutate",". Le plugin Kotlin d'arcmutate (",[227,333,336],{"href":334,"rel":335},"https:\u002F\u002Fdocs.arcmutate.com\u002Fdocs\u002Fkotlin.html",[231],[78,337,338],{},"com.arcmutate:pitest-kotlin-plugin",") va bien plus loin : il comprend les fonctions ",[227,341,344],{"href":342,"rel":343},"https:\u002F\u002Fkotlinlang.org\u002Fdocs\u002Finline-functions.html",[231],[78,345,346],{},"inline"," (le compilateur copie leur corps à chaque appel, ce qui multiplie les mutants sans support spécifique), les coroutines, le destructuring, ",[78,349,350],{},"lateinit",", les branches non couvertes des ",[78,353,354],{},"sealed class"," et des enums.",[12,357,358],{},"Sans ce support, pitest génèrerait un mutant par copie inlinée plutôt que sur la définition d'origine : explosant inutilement le nombre de mutants à analyser. En clair, il filtre le bruit propre à Kotlin que pitest seul ne sait pas distinguer du vrai code métier.",[12,360,361,362,365],{},"Le revers, c'est le modèle : arcmutate est un produit commercial, facturé au nombre de personnes ayant accès au repo. À noter quand même, c'est ",[25,363,364],{},"gratuit pour les projets open source"," et une licence d'évaluation s'obtient par mail. Pour des projets pro, ça veut dire arbitrer entre une licence payante et le mutation testing en Java « pur » sur les modules qui s'y prêtent. C'est typiquement le genre de coût caché qu'on n'anticipe pas quand on choisit Kotlin pour un projet.",[12,367,368],{},"Une bonne assertion suffit à corriger l'exemple :",[71,370,372],{"className":73,"code":371,"language":75,"meta":76,"style":76},"applyDiscount(100.0, 0.2) shouldBe 80.0\n",[78,373,374],{"__ignoreMap":76},[81,375,376,379,381,383,385,387,390],{"class":83,"line":84},[81,377,378],{"class":91},"applyDiscount",[81,380,165],{"class":95},[81,382,184],{"class":183},[81,384,187],{"class":95},[81,386,190],{"class":183},[81,388,389],{"class":95},") shouldBe ",[81,391,392],{"class":183},"80.0\n",[12,394,395,396,398],{},"Et là, le mutant ",[78,397,217],{}," meurt instantanément.",[400,401,403],"h3",{"id":402},"le-prix-à-payer","Le prix à payer",[12,405,406],{},"Soyons honnêtes sur ce que ça coûte. Chaque mutant exige de relancer une partie de la suite de tests, et un projet de taille moyenne en génère vite des centaines : le temps d'exécution explose par rapport à un simple run de tests. Première parade, faire le tri. Limiter le scope aux packages critiques, choisir un groupe de mutateurs pertinent plutôt que de tout activer (plus de mutateurs, c'est une analyse plus fine mais un temps d'exécution qui s'envole), et exploiter l'analyse incrémentale de pitest pour ne muter que le code qui a changé.",[12,408,409],{},"Deuxième parade, la parallélisation. Mais elle ne fait que déplacer le problème vers la CI : dans le cloud, c'est la facture qui grimpe ; en self-hosted, ce sont les runners qui saturent.",[12,411,412],{},"En pratique, le mutation testing se prête mieux à un job nightly ou hebdomadaire qu'à un déclenchement à chaque PR.",[400,414,416],{"id":415},"le-mutation-testing-est-il-la-seule-réponse","Le mutation testing est-il la seule réponse ?",[12,418,419],{},"C'est la question que je me pose depuis le talk : existe-t-il d'autres moyens de vérifier que les tests vérifient vraiment quelque chose ?",[12,421,422,423,426,427,431],{},"La version artisanale existe depuis toujours : c'est la discipline du ",[15,424,425],{},"red"," en ",[227,428,276],{"href":429,"rel":430},"https:\u002F\u002Fhoppr-tech.notion.site\u002FFormation-TDD-HoppR-19ff4462cd388034b74eecb49e31630d",[231],". Un test qu'on a vu échouer au moins une fois a prouvé qu'il était capable de détecter quelque chose. Le mutation testing ne fait au fond qu'automatiser ce « voir le test échouer », a posteriori et à grande échelle, pour tous les tests qu'on n'a pas écrits en TDD.",[12,433,434,435,438,439,444],{},"L'autre piste, complémentaire, c'est le ",[25,436,437],{},"property-based testing (PBT)",". Au lieu de vérifier un exemple choisi à la main, on affirme une propriété qui doit tenir sur des centaines d'entrées générées automatiquement. ",[227,440,443],{"href":441,"rel":442},"https:\u002F\u002Fkotest.io\u002Fdocs\u002Fproptest\u002Fproperty-based-testing.html",[231],"Kotest"," le supporte nativement :",[71,446,448],{"className":73,"code":447,"language":75,"meta":76,"style":76},"test(\"la remise diminue toujours le prix\") {\n    checkAll(Arb.double(1.0..10_000.0), Arb.double(0.01..0.9)) { price, rate ->\n        applyDiscount(price, rate) shouldBeLessThan price\n    }\n}\n",[78,449,450,462,505,512,516],{"__ignoreMap":76},[81,451,452,455,457,460],{"class":83,"line":84},[81,453,454],{"class":91},"test",[81,456,165],{"class":95},[81,458,459],{"class":168},"\"la remise diminue toujours le prix\"",[81,461,172],{"class":95},[81,463,464,467,470,473,475,478,481,484,487,489,491,494,496,499,502],{"class":83,"line":116},[81,465,466],{"class":91},"    checkAll",[81,468,469],{"class":95},"(Arb.",[81,471,472],{"class":91},"double",[81,474,165],{"class":95},[81,476,477],{"class":183},"1.0",[81,479,480],{"class":87},"..",[81,482,483],{"class":183},"10_000.0",[81,485,486],{"class":95},"), Arb.",[81,488,472],{"class":91},[81,490,165],{"class":95},[81,492,493],{"class":183},"0.01",[81,495,480],{"class":87},[81,497,498],{"class":183},"0.9",[81,500,501],{"class":95},")) { price, rate ",[81,503,504],{"class":87},"->\n",[81,506,507,509],{"class":83,"line":134},[81,508,178],{"class":91},[81,510,511],{"class":95},"(price, rate) shouldBeLessThan price\n",[81,513,514],{"class":83,"line":141},[81,515,202],{"class":95},[81,517,518],{"class":83,"line":159},[81,519,520],{"class":95},"}\n",[12,522,523,524,526,527,530,531,536,530,539,544,547],{},"Ce test tue le mutant ",[78,525,217],{}," lui aussi, mais il explore en prime tout un espace d'entrées auquel je n'aurais pas pensé. Les deux approches ne répondent pas à la même question : ",[25,528,529],{},"le mutation testing évalue la qualité des tests"," ",[15,532,533],{},[25,534,535],{},"existants",[25,537,538],{},", le property-based testing rend les tests plus difficiles à tromper",[15,540,541],{},[25,542,543],{},"dès l'écriture",[25,545,546],{},"."," Rien n'empêche de combiner : écrire des propriétés là où le domaine s'y prête, et laisser le mutation testing juger le reste.",[12,549,550,551,558,559,562,563,566],{},"Petit détour par l'écosystème, parce que le support du PBT n'est pas uniforme sur la JVM. Côté Java, il n'y a rien dans le langage ni dans JUnit : il faut une lib dédiée, et la référence est ",[227,552,555],{"href":553,"rel":554},"https:\u002F\u002Fjqwik.net\u002F",[231],[25,556,557],{},"jqwik",", qui s'intègre comme moteur de test sur la plateforme JUnit 5. On y déclare une propriété avec ",[78,560,561],{},"@Property"," au lieu de ",[78,564,565],{},"@Test"," :",[71,568,572],{"className":569,"code":570,"language":571,"meta":76,"style":76},"language-java shiki shiki-themes github-dark-default","@Property\nvoid laRemiseDiminueLePrix(@ForAll @DoubleRange(min = 1) double price,\n                           @ForAll @DoubleRange(min = 0.01, max = 0.9) double rate) {\n    assertThat(applyDiscount(price, rate)).isLessThan(price);\n}\n","java",[78,573,574,582,621,658,676],{"__ignoreMap":76},[81,575,576,579],{"class":83,"line":84},[81,577,578],{"class":95},"@",[81,580,581],{"class":87},"Property\n",[81,583,584,587,590,593,596,599,602,604,607,610,613,616,618],{"class":83,"line":116},[81,585,586],{"class":87},"void",[81,588,589],{"class":91}," laRemiseDiminueLePrix",[81,591,592],{"class":95},"(@",[81,594,595],{"class":87},"ForAll",[81,597,598],{"class":95}," @",[81,600,601],{"class":87},"DoubleRange",[81,603,165],{"class":95},[81,605,606],{"class":183},"min",[81,608,609],{"class":87}," =",[81,611,612],{"class":183}," 1",[81,614,615],{"class":95},") ",[81,617,472],{"class":87},[81,619,620],{"class":95}," price,\n",[81,622,623,626,628,630,632,634,636,638,641,643,646,648,651,653,655],{"class":83,"line":134},[81,624,625],{"class":95},"                           @",[81,627,595],{"class":87},[81,629,598],{"class":95},[81,631,601],{"class":87},[81,633,165],{"class":95},[81,635,606],{"class":183},[81,637,609],{"class":87},[81,639,640],{"class":183}," 0.01",[81,642,187],{"class":95},[81,644,645],{"class":183},"max",[81,647,609],{"class":87},[81,649,650],{"class":183}," 0.9",[81,652,615],{"class":95},[81,654,472],{"class":87},[81,656,657],{"class":95}," rate) {\n",[81,659,660,663,665,667,670,673],{"class":83,"line":141},[81,661,662],{"class":91},"    assertThat",[81,664,165],{"class":95},[81,666,378],{"class":91},[81,668,669],{"class":95},"(price, rate)).",[81,671,672],{"class":91},"isLessThan",[81,674,675],{"class":95},"(price);\n",[81,677,678],{"class":83,"line":159},[81,679,520],{"class":95},[12,681,682,683,686,687,690],{},"jqwik fonctionne aussi très bien en Kotlin, et son arsenal est plus fourni que celui de Kotest : ",[15,684,685],{},"shrinking"," plus efficace (la réduction du contre-exemple vers le cas minimal qui fait échouer), générateurs récursifs, tests ",[15,688,689],{},"stateful",", statistiques sur les données générées. Le revers : jqwik est aujourd'hui en mode maintenance, sans développement de nouvelles fonctionnalités faute de financement.",[12,692,693],{},"Le choix dépend donc surtout de ta base de tests existante. Si tu es déjà sur Kotest, son PBT intégré suffit largement pour démarrer et évite d'empiler deux moteurs de test. Si tu es sur JUnit 5 (le cas par défaut en Java, et fréquent en Kotlin aussi), jqwik s'ajoute sans friction et offre les fonctionnalités les plus avancées. Dans les deux cas, l'idée reste la même : arrêter de tester un exemple pour tester une vérité générale.",[12,695,696,697,700,701,707,708,713],{},"Deux autres angles, plus discrets, complètent le tableau. D'abord, l'",[25,698,699],{},"analyse statique du code de test"," : détecter les tests sans assertion, ceux qui passent quoi qu'il arrive ou qui croulent sous les mocks, c'est juger la pertinence d'un test sans même l'exécuter (detekt a des règles pour ça, joli retour de la section précédente). Ensuite, la *",[15,702,703,704],{},"chasse aux tests ",[15,705,706],{},"flaky"," : un test non déterministe est pire qu'absent, il érode la confiance dans toute la suite. Plus loin sur le même axe que le property-based testing, le fuzzing__ (",[227,709,712],{"href":710,"rel":711},"https:\u002F\u002Fgithub.com\u002FCodeIntelligenceTesting\u002Fjazzer",[231],"Jazzer"," sur la JVM) explore les entrées qui maximisent la couverture pour faire tomber le code là où on ne l'attend pas.",[715,716,717,727],"blockquote",{},[12,718,719,722,723,726],{},[25,720,721],{},"Mon avis"," : le mutation testing a longtemps eu une réputation de truc lent et inexploitable (des rapports de centaines de mutants survivants, bon courage pour trier). C'est précisément là qu'un LLM avec le contexte de la codebase change la donne : lui faire lire le rapport, écarter les mutants équivalents et pointer les ",[15,724,725],{},"vrais"," trous de test transforme un outil de niche en quelque chose qu'on peut industrialiser. Le talk le mentionnait, et je suis convaincu que c'est ce qui va le démocratiser.",[12,728,729],{},"Un dernier point pratique : inutile de vouloir tout corriger d'un coup. Un backlog de mutants survivants se résorbe comme n'importe quelle dette : progressivement, en faisant baisser l'entropie logicielle à chaque itération plutôt qu'en bloquant la livraison.",[37,731],{},[40,733,735],{"id":734},"_2-mon-code-compile-mais-compiler-nest-pas-valider","2. « Mon code compile », mais compiler n'est pas valider",[12,737,738],{},[251,739],{"alt":740,"src":741},"Illustration en deux parties expliquant l'analyse statique. À gauche, \"Le compilateur seul\" : un bloc de code dark mode montre val user = repository.find(id) puis val name = user.name avec un commentaire \"NPE potentielle !\". En dessous, un petit robot bleu souriant tient un badge \"✅ COMPILES\" pendant qu'une bombe à la mèche allumée et au sourire malveillant passe inaperçue. Légende : \"Ça compile, mais le bug passe\". À droite, \"Compilateur + analyse statique\" : même code, mais le robot porte maintenant un badge de détective et tient une loupe, bloquant la bombe surprise. Une bulle affiche \"NullPointerException detected at build time\". Légende : \"Bug détecté avant l'exécution\".","\u002Fcontent-assets\u002F2026-06-23-le-vert-ne-suffit-pas-3-facons-de-douter-de-son-code\u002Fassets\u002Fimg2.webp",[12,743,744,745,752,753,760,761,764,765,768,769,772],{},"Deuxième couche de fausse confiance : ça compile, donc c'est bon. Dans « Et si vos erreurs se faisaient attraper avant l'exécution ? », ",[227,746,749],{"href":747,"rel":748},"https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fle-pinpin\u002F",[231],[25,750,751],{},"Remi Taniel"," démonte l'idée avec ",[227,754,757],{"href":755,"rel":756},"https:\u002F\u002Ferrorprone.info\u002F",[231],[25,758,759],{},"Error Prone",", l'analyseur statique de Google pour Java. Plus de 500 ",[15,762,763],{},"bug patterns"," détectés au moment du build, sur des erreurs que ",[78,766,767],{},"javac"," laisse tranquillement passer. Sur un projet aussi mûr que ",[78,770,771],{},"commons-lang3",", la démo sortait une centaine de warnings et quelques erreurs critiques. Même les gros projets en ont.",[12,774,775,776,783,784,791,792,795,796,799,800,807,808,811,812],{},"L'écosystème qui gravite autour est costaud : ",[227,777,780],{"href":778,"rel":779},"https:\u002F\u002Fgithub.com\u002Fuber\u002FNullAway",[231],[25,781,782],{},"NullAway"," (par Uber) pour traquer les NPE au build, ",[227,785,788],{"href":786,"rel":787},"https:\u002F\u002Ferrorprone.info\u002Fdocs\u002Frefaster",[231],[25,789,790],{},"Refaster"," pour écrire ses propres règles de refactoring sous forme de templates ",[78,793,794],{},"@BeforeTemplate"," \u002F ",[78,797,798],{},"@AfterTemplate",", et le ",[227,801,804],{"href":802,"rel":803},"https:\u002F\u002Fgithub.com\u002FPicnicSupermarket\u002Ferror-prone-support",[231],[25,805,806],{},"Picnic Error Prone Support"," de la communauté qui ajoute des dizaines de règles. Le mantra du talk résume tout : ",[15,809,810],{},"« outillez le compilateur, pas que le dev »",", et son corollaire très 2026 : ",[15,813,814],{},"« l'IA génère le code, l'analyse statique le vérifie. »",[12,816,817,818,821,822,824],{},"Et c'est ici que ça devient intéressant pour moi : ",[25,819,820],{},"Error Prone et NullAway sont des outils pensés pour Java."," Concrètement, ce sont des plugins de ",[78,823,767],{}," qui analysent le code source au moment de la compilation. Kotlin ayant son propre compilateur, ils ne voient jamais mon code Kotlin (contrairement à pitest qui, lui, travaille sur le bytecode et reste donc agnostique du langage). Alors, qu'est-ce que je récupère vraiment de ce talk dans ma stack ?",[12,826,827,830,831,834,835,840],{},[25,828,829],{},"D'abord, une partie du problème disparaît dans le langage."," NullAway existe parce que le ",[78,832,833],{},"null"," est un trou noir en Java. En Kotlin, ",[227,836,839],{"href":837,"rel":838},"https:\u002F\u002Fkotlinlang.org\u002Fdocs\u002Fnull-safety.html",[231],"la nullabilité est dans le système de types",", et le compilateur refuse tout simplement de compiler le code dangereux :",[71,842,844],{"className":73,"code":843,"language":75,"meta":76,"style":76},"val user: User? = repository.findByName(\"flo\")\n\n\u002F\u002F Ne compile pas : user peut être null\nval length = user.name.length\n\n\u002F\u002F Le compilateur t'oblige à traiter le cas\nval length = user?.name?.length ?: 0\n",[78,845,846,877,881,887,899,903,908],{"__ignoreMap":76},[81,847,848,851,854,857,860,863,866,869,871,874],{"class":83,"line":84},[81,849,850],{"class":87},"val",[81,852,853],{"class":95}," user: ",[81,855,856],{"class":99},"User",[81,858,859],{"class":95},"? ",[81,861,862],{"class":87},"=",[81,864,865],{"class":95}," repository.",[81,867,868],{"class":91},"findByName",[81,870,165],{"class":95},[81,872,873],{"class":168},"\"flo\"",[81,875,876],{"class":95},")\n",[81,878,879],{"class":83,"line":116},[81,880,138],{"emptyLinePlaceholder":137},[81,882,883],{"class":83,"line":134},[81,884,886],{"class":885},"sH3jZ","\u002F\u002F Ne compile pas : user peut être null\n",[81,888,889,891,894,896],{"class":83,"line":141},[81,890,850],{"class":87},[81,892,893],{"class":95}," length ",[81,895,862],{"class":87},[81,897,898],{"class":95}," user.name.length\n",[81,900,901],{"class":83,"line":159},[81,902,138],{"emptyLinePlaceholder":137},[81,904,905],{"class":83,"line":175},[81,906,907],{"class":885},"\u002F\u002F Le compilateur t'oblige à traiter le cas\n",[81,909,910,912,914,916,919],{"class":83,"line":199},[81,911,850],{"class":87},[81,913,893],{"class":95},[81,915,862],{"class":87},[81,917,918],{"class":95}," user?.name?.length ?: ",[81,920,921],{"class":183},"0\n",[12,923,924,925,928,929,932],{},"Ce que NullAway rattrape ",[15,926,927],{},"a posteriori"," par analyse statique, Kotlin l'interdit ",[15,930,931],{},"a priori",". Tout un pan d'Error Prone devient sans objet, non pas parce que le problème est moins grave, mais parce que le langage l'a absorbé.",[12,934,935,938,939,942,943,950],{},[25,936,937],{},"Ensuite, ce qui reste, c'est detekt."," Parce que tout Error Prone ne se résume pas à la null-safety : il y a les bugs subtils (comparaisons douteuses, ressources non fermées) et surtout l'application des ",[25,940,941],{},"conventions d'équipe",". Ça, le langage ne le fait pas pour toi. En Kotlin, l'outil c'est ",[227,944,947],{"href":945,"rel":946},"https:\u002F\u002Fdetekt.dev\u002F",[231],[25,948,949],{},"detekt",", qui s'active et se configure dans le même esprit d'adoption progressive prônée par le talk :",[71,952,956],{"className":953,"code":954,"language":955,"meta":76,"style":76},"language-yaml shiki shiki-themes github-dark-default","# detekt.yml\npotential-bugs:\n  active: true\n  UnsafeCallOnNullableType:\n    active: true\n  CastToNullableType:\n    active: true\n\nstyle:\n  ReturnCount:\n    active: true\n    max: 2\n","yaml",[78,957,958,963,972,983,990,999,1006,1014,1018,1026,1034,1043],{"__ignoreMap":76},[81,959,960],{"class":83,"line":84},[81,961,962],{"class":885},"# detekt.yml\n",[81,964,965,969],{"class":83,"line":116},[81,966,968],{"class":967},"sPWt5","potential-bugs",[81,970,971],{"class":95},":\n",[81,973,974,977,980],{"class":83,"line":134},[81,975,976],{"class":967},"  active",[81,978,979],{"class":95},": ",[81,981,982],{"class":183},"true\n",[81,984,985,988],{"class":83,"line":141},[81,986,987],{"class":967},"  UnsafeCallOnNullableType",[81,989,971],{"class":95},[81,991,992,995,997],{"class":83,"line":159},[81,993,994],{"class":967},"    active",[81,996,979],{"class":95},[81,998,982],{"class":183},[81,1000,1001,1004],{"class":83,"line":175},[81,1002,1003],{"class":967},"  CastToNullableType",[81,1005,971],{"class":95},[81,1007,1008,1010,1012],{"class":83,"line":199},[81,1009,994],{"class":967},[81,1011,979],{"class":95},[81,1013,982],{"class":183},[81,1015,1016],{"class":83,"line":205},[81,1017,138],{"emptyLinePlaceholder":137},[81,1019,1021,1024],{"class":83,"line":1020},9,[81,1022,1023],{"class":967},"style",[81,1025,971],{"class":95},[81,1027,1029,1032],{"class":83,"line":1028},10,[81,1030,1031],{"class":967},"  ReturnCount",[81,1033,971],{"class":95},[81,1035,1037,1039,1041],{"class":83,"line":1036},11,[81,1038,994],{"class":967},[81,1040,979],{"class":95},[81,1042,982],{"class":183},[81,1044,1046,1049,1051],{"class":83,"line":1045},12,[81,1047,1048],{"class":967},"    max",[81,1050,979],{"class":95},[81,1052,1053],{"class":183},"2\n",[12,1055,1056,1057,1064,1065,1068],{},"Ce qui rend detekt facile à adopter, c'est qu'il s'intègre dans à peu près n'importe quel projet : un plugin Gradle, Maven ou Bazel, et il tourne aussi bien sur de l'Android, du JVM pur, du Kotlin\u002FJS, du natif ou du multiplateforme. Mais le détail qui fait vraiment écho au talk sur Error Prone, c'est le ",[227,1058,1061],{"href":1059,"rel":1060},"https:\u002F\u002Fdetekt.dev\u002Fdocs\u002Fintroduction\u002Fbaseline\u002F",[231],[25,1062,1063],{},"baseline",". Sur une codebase existante, brancher un analyseur d'un coup, c'est des centaines de warnings et une CI rouge dès le premier jour. Le fichier baseline de detekt fige les problèmes connus dans un instantané : l'outil ne casse plus la build que sur les ",[25,1066,1067],{},"nouveaux"," écarts, et tu résorbes la dette à ton rythme. C'est exactement l'adoption progressive que prônait le talk (erreurs seulement, on désactive les warnings, on corrige le critique, on configure les exceptions), sauf qu'ici elle est offerte par l'outil.",[12,1070,1071,1072,1075,1076,1079,1080,1083],{},"Deux autres atouts au quotidien. detekt embarque un plugin ",[25,1073,1074],{},"formatting"," (qui s'appuie sur ktlint) capable de corriger automatiquement une bonne partie des écarts de style, donc moins de bikeshedding en review. Et il exporte ses rapports en HTML, XML, Markdown ou ",[25,1077,1078],{},"SARIF",", ce dernier format étant directement digéré par GitHub pour afficher les findings dans l'onglet Security et en annotation de PR. Détail qui n'en est pas un, après la section précédente : contrairement à arcmutate, detekt est ",[25,1081,1082],{},"entièrement open source et porté par la communauté",". Aucune licence à arbitrer.",[12,1085,1086,1087,1090],{},"Et là où Error Prone a Refaster pour coder ses propres règles, detekt permet d'écrire des ",[25,1088,1089],{},"custom rules",", l'équivalent direct, pour faire respecter une guideline maison au build plutôt qu'en review :",[71,1092,1094],{"className":73,"code":1093,"language":75,"meta":76,"style":76},"class NoPrintlnRule(config: Config) : Rule(config) {\n    override val issue = Issue(\n        \"NoPrintln\",\n        Severity.Style,\n        \"Utilise un logger, pas println.\",\n        Debt.FIVE_MINS\n    )\n\n    override fun visitCallExpression(expression: KtCallExpression) {\n        super.visitCallExpression(expression)\n        if (expression.calleeExpression?.text == \"println\") {\n            report(CodeSmell(issue, Entity.from(expression), issue.description))\n        }\n    }\n}\n",[78,1095,1096,1122,1141,1149,1154,1161,1166,1171,1175,1193,1206,1222,1241,1247,1252],{"__ignoreMap":76},[81,1097,1098,1100,1103,1106,1109,1112,1115,1117,1120],{"class":83,"line":84},[81,1099,144],{"class":87},[81,1101,1102],{"class":99}," NoPrintlnRule",[81,1104,1105],{"class":95},"(config: ",[81,1107,1108],{"class":99},"Config",[81,1110,1111],{"class":95},") : ",[81,1113,1114],{"class":99},"Rule",[81,1116,165],{"class":95},[81,1118,1119],{"class":99},"config",[81,1121,172],{"class":95},[81,1123,1124,1127,1130,1133,1135,1138],{"class":83,"line":116},[81,1125,1126],{"class":87},"    override",[81,1128,1129],{"class":87}," val",[81,1131,1132],{"class":95}," issue ",[81,1134,862],{"class":87},[81,1136,1137],{"class":91}," Issue",[81,1139,1140],{"class":95},"(\n",[81,1142,1143,1146],{"class":83,"line":134},[81,1144,1145],{"class":168},"        \"NoPrintln\"",[81,1147,1148],{"class":95},",\n",[81,1150,1151],{"class":83,"line":141},[81,1152,1153],{"class":95},"        Severity.Style,\n",[81,1155,1156,1159],{"class":83,"line":159},[81,1157,1158],{"class":168},"        \"Utilise un logger, pas println.\"",[81,1160,1148],{"class":95},[81,1162,1163],{"class":83,"line":175},[81,1164,1165],{"class":95},"        Debt.FIVE_MINS\n",[81,1167,1168],{"class":83,"line":199},[81,1169,1170],{"class":95},"    )\n",[81,1172,1173],{"class":83,"line":205},[81,1174,138],{"emptyLinePlaceholder":137},[81,1176,1177,1179,1182,1185,1188,1191],{"class":83,"line":1020},[81,1178,1126],{"class":87},[81,1180,1181],{"class":87}," fun",[81,1183,1184],{"class":91}," visitCallExpression",[81,1186,1187],{"class":95},"(expression: ",[81,1189,1190],{"class":99},"KtCallExpression",[81,1192,172],{"class":95},[81,1194,1195,1198,1200,1203],{"class":83,"line":1028},[81,1196,1197],{"class":183},"        super",[81,1199,546],{"class":95},[81,1201,1202],{"class":91},"visitCallExpression",[81,1204,1205],{"class":95},"(expression)\n",[81,1207,1208,1211,1214,1217,1220],{"class":83,"line":1036},[81,1209,1210],{"class":87},"        if",[81,1212,1213],{"class":95}," (expression.calleeExpression?.text ",[81,1215,1216],{"class":87},"==",[81,1218,1219],{"class":168}," \"println\"",[81,1221,172],{"class":95},[81,1223,1224,1227,1229,1232,1235,1238],{"class":83,"line":1045},[81,1225,1226],{"class":91},"            report",[81,1228,165],{"class":95},[81,1230,1231],{"class":91},"CodeSmell",[81,1233,1234],{"class":95},"(issue, Entity.",[81,1236,1237],{"class":91},"from",[81,1239,1240],{"class":95},"(expression), issue.description))\n",[81,1242,1244],{"class":83,"line":1243},13,[81,1245,1246],{"class":95},"        }\n",[81,1248,1250],{"class":83,"line":1249},14,[81,1251,202],{"class":95},[81,1253,1255],{"class":83,"line":1254},15,[81,1256,520],{"class":95},[12,1258,1259,1260,1263,1264,1267],{},"Une limite à connaître pour que la comparaison reste honnête : par défaut, detekt analyse l'arbre syntaxique sans résolution de types. Les règles les plus puissantes, celles qui ont besoin de savoir ",[15,1261,1262],{},"ce qu'est"," une expression et pas seulement comment elle s'écrit, demandent d'activer le mode ",[15,1265,1266],{},"type resolution",", plus lourd à mettre en place. Error Prone, lui, vit dans le compilateur et profite de tout le contexte de compilation. Kotlin absorbe donc la null-safety dans le langage, mais perd un peu en finesse d'analyse statique au passage. Le trade-off me semble largement gagnant, mais il existe.",[715,1269,1270],{},[12,1271,1272,1274,1275,1278],{},[25,1273,721],{}," : la leçon du talk survit au changement de langage, le compilateur seul ne suffit pas, il faut l'outiller. Mais le détour par Kotlin révèle une hiérarchie des garanties : ",[25,1276,1277],{},"un bug rendu impossible par le langage vaut mieux qu'un bug détecté par un outil, qui vaut lui-même mieux qu'un bug attrapé en code review."," Plus la garantie descend vers le langage, moins elle dépend de la discipline humaine. La null-safety native de Kotlin, c'est NullAway rendu inutile. Le vrai job de detekt, du coup, ce n'est pas de rejouer Error Prone : c'est de verrouiller l'étage que le langage ne capturera jamais, les conventions d'équipe.",[37,1280],{},[40,1282,1284],{"id":1283},"_3-ça-marche-en-local-mais-ça-tient-sous-la-panne","3. « Ça marche en local », mais ça tient sous la panne ?",[12,1286,1287,1288,1295],{},"Dernière illusion, la plus humble : ça tourne sur ma machine, ça tourne en recette, donc ça tournera. Dans « Déchaînez le Chaos : tester la résilience de votre application avec Chaos Monkey », ",[227,1289,1292],{"href":1290,"rel":1291},"https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Ferwan-le-tutour\u002F",[231],[25,1293,1294],{},"Erwan Le Tutour"," part du principe inverse : la panne n'est pas un accident, c'est une condition d'exploitation. Autant la provoquer soi-même, à froid, plutôt que de la découvrir un vendredi soir.",[12,1297,1298,1299,1302,1303,1310,1311,1314],{},"L'histoire vient de Netflix et de son ",[25,1300,1301],{},"Chaos Monkey"," (open source depuis 2012), décliné en une vraie ",[227,1304,1307],{"href":1305,"rel":1306},"https:\u002F\u002Fnetflixtechblog.com\u002Fthe-netflix-simian-army-16e57fbab116",[231],[15,1308,1309],{},"Simian Army"," : Chaos Kong, Conformity Monkey, Security Monkey… Et le talk rappelait qu'en France, la SNCF passe ",[25,1312,1313],{},"un mois de chaos engineering"," avant chaque grosse mise en prod. Le chaos comme quotidien, littéralement.",[12,1316,1317],{},[251,1318],{"alt":1319,"src":1320},"Illustration en deux parties expliquant le chaos engineering. À gauche, \"En local \u002F en recette\" : un dashboard dark mode affiche deux lignes de métriques toutes au vert — Local : Uptime 100%, Latency 8ms, Errors 0% ; Recette : Uptime 100%, Latency 23ms, Errors 0%. En dessous, un petit robot vert souriant fait un pouce levé à côté d'un serveur. Légende : \"Ça marche en local... \". À droite, \"Chaos Monkey lâché\" : le même dashboard mais avec toutes les métriques dans le rouge — Local : Uptime 12%, Latency 4800ms, Errors 73% ; Recette : Uptime 42%, Latency 3200ms, Errors 58%. Un singe malicieux est assis sur le dashboard en tirant des fils. En dessous, le même robot vert panique avec les yeux écarquillés, à côté d'un badge circuit breaker ⚡ avec la mention \"Circuit breaker ON\". Légende : \"La résilience mise à l'épreuve ","\u002Fcontent-assets\u002F2026-06-23-le-vert-ne-suffit-pas-3-facons-de-douter-de-son-code\u002Fassets\u002Fimg3.webp",[12,1322,1323,1324,1331,1332,1335,1336,187,1339,187,1342,1345,1346,1349],{},"La bonne nouvelle pour ma stack : ",[227,1325,1328],{"href":1326,"rel":1327},"https:\u002F\u002Fcodecentric.github.io\u002Fchaos-monkey-spring-boot\u002Flatest\u002F",[231],[25,1329,1330],{},"Chaos Monkey for Spring Boot"," (par Codecentric) rend ça quasi gratuit. Une dépendance, des ",[15,1333,1334],{},"watchers"," qui s'injectent sur tes ",[78,1337,1338],{},"@Service",[78,1340,1341],{},"@Repository",[78,1343,1344],{},"@RestController"," sans toucher au code métier, et des ",[15,1347,1348],{},"assauts"," configurables : latence, exceptions, pression mémoire.",[71,1351,1353],{"className":73,"code":1352,"language":75,"meta":76,"style":76},"\u002F\u002F build.gradle.kts\ndependencies {\n    implementation(\"de.codecentric:chaos-monkey-spring-boot:4.0.0\")\n}\n",[78,1354,1355,1360,1368,1380],{"__ignoreMap":76},[81,1356,1357],{"class":83,"line":84},[81,1358,1359],{"class":885},"\u002F\u002F build.gradle.kts\n",[81,1361,1362,1365],{"class":83,"line":116},[81,1363,1364],{"class":91},"dependencies",[81,1366,1367],{"class":95}," {\n",[81,1369,1370,1373,1375,1378],{"class":83,"line":134},[81,1371,1372],{"class":91},"    implementation",[81,1374,165],{"class":95},[81,1376,1377],{"class":168},"\"de.codecentric:chaos-monkey-spring-boot:4.0.0\"",[81,1379,876],{"class":95},[81,1381,1382],{"class":83,"line":141},[81,1383,520],{"class":95},[71,1385,1387],{"className":953,"code":1386,"language":955,"meta":76,"style":76},"# application-chaos.yml\nchaos:\n  monkey:\n    watcher:\n      service: true\n      repository: true\n    assaults:\n      level: 5            # 1 requête sur 5 est attaquée\n      latency-active: true\n      latency-range-start: 1000\n      latency-range-end: 3000\n      exceptions-active: true\n",[78,1388,1389,1394,1401,1408,1415,1424,1433,1440,1453,1462,1472,1482],{"__ignoreMap":76},[81,1390,1391],{"class":83,"line":84},[81,1392,1393],{"class":885},"# application-chaos.yml\n",[81,1395,1396,1399],{"class":83,"line":116},[81,1397,1398],{"class":967},"chaos",[81,1400,971],{"class":95},[81,1402,1403,1406],{"class":83,"line":134},[81,1404,1405],{"class":967},"  monkey",[81,1407,971],{"class":95},[81,1409,1410,1413],{"class":83,"line":141},[81,1411,1412],{"class":967},"    watcher",[81,1414,971],{"class":95},[81,1416,1417,1420,1422],{"class":83,"line":159},[81,1418,1419],{"class":967},"      service",[81,1421,979],{"class":95},[81,1423,982],{"class":183},[81,1425,1426,1429,1431],{"class":83,"line":175},[81,1427,1428],{"class":967},"      repository",[81,1430,979],{"class":95},[81,1432,982],{"class":183},[81,1434,1435,1438],{"class":83,"line":199},[81,1436,1437],{"class":967},"    assaults",[81,1439,971],{"class":95},[81,1441,1442,1445,1447,1450],{"class":83,"line":205},[81,1443,1444],{"class":967},"      level",[81,1446,979],{"class":95},[81,1448,1449],{"class":183},"5",[81,1451,1452],{"class":885},"            # 1 requête sur 5 est attaquée\n",[81,1454,1455,1458,1460],{"class":83,"line":1020},[81,1456,1457],{"class":967},"      latency-active",[81,1459,979],{"class":95},[81,1461,982],{"class":183},[81,1463,1464,1467,1469],{"class":83,"line":1028},[81,1465,1466],{"class":967},"      latency-range-start",[81,1468,979],{"class":95},[81,1470,1471],{"class":183},"1000\n",[81,1473,1474,1477,1479],{"class":83,"line":1036},[81,1475,1476],{"class":967},"      latency-range-end",[81,1478,979],{"class":95},[81,1480,1481],{"class":183},"3000\n",[81,1483,1484,1487,1489],{"class":83,"line":1045},[81,1485,1486],{"class":967},"      exceptions-active",[81,1488,979],{"class":95},[81,1490,982],{"class":183},[12,1492,1493,1494,1496,1497,1502],{},"On active le profil ",[78,1495,1398],{}," en recette, et on regarde l'appli se faire maltraiter. Le confort, c'est que tout se pilote à chaud : les assauts s'activent, se désactivent et se règlent via les endpoints ",[227,1498,1501],{"href":1499,"rel":1500},"https:\u002F\u002Fdocs.spring.io\u002Fspring-boot\u002Fhow-to\u002Factuator.html",[231],"Actuator",", sans redéployer. On peut viser précisément un composant, doser le niveau de déterminisme (toutes les requêtes ou une sur cinq) et choisir le type d'attaque (latence, exceptions, pression mémoire). De quoi mener une expérience ciblée plutôt que de tout casser en même temps.",[12,1504,1505,1506,150,1509,1516],{},"L'autre moitié de l'équation, c'est la ",[25,1507,1508],{},"résilience",[227,1510,1513],{"href":1511,"rel":1512},"https:\u002F\u002Fresilience4j.readme.io\u002F",[231],[25,1514,1515],{},"Resilience4j",", la librairie Java de référence pour poser des garde-fous là où le chaos a révélé des fragilités. Elle s'utilise aussi bien en Java qu'en Kotlin, via les mêmes annotations Spring.",[71,1518,1520],{"className":73,"code":1519,"language":75,"meta":76,"style":76},"@CircuitBreaker(name = \"paymentService\", fallbackMethod = \"paymentFallback\")\n@Retry(name = \"paymentService\")\nfun callPayment(order: Order): PaymentResult =\n    paymentClient.charge(order)\n\nfun paymentFallback(order: Order, ex: Throwable): PaymentResult =\n    PaymentResult.deferred(order)\n",[78,1521,1522,1545,1558,1578,1589,1593,1616],{"__ignoreMap":76},[81,1523,1524,1527,1530,1532,1535,1538,1540,1543],{"class":83,"line":84},[81,1525,1526],{"class":99},"@CircuitBreaker",[81,1528,1529],{"class":95},"(name ",[81,1531,862],{"class":87},[81,1533,1534],{"class":168}," \"paymentService\"",[81,1536,1537],{"class":95},", fallbackMethod ",[81,1539,862],{"class":87},[81,1541,1542],{"class":168}," \"paymentFallback\"",[81,1544,876],{"class":95},[81,1546,1547,1550,1552,1554,1556],{"class":83,"line":116},[81,1548,1549],{"class":99},"@Retry",[81,1551,1529],{"class":95},[81,1553,862],{"class":87},[81,1555,1534],{"class":168},[81,1557,876],{"class":95},[81,1559,1560,1562,1565,1568,1571,1573,1576],{"class":83,"line":134},[81,1561,88],{"class":87},[81,1563,1564],{"class":91}," callPayment",[81,1566,1567],{"class":95},"(order: ",[81,1569,1570],{"class":99},"Order",[81,1572,108],{"class":95},[81,1574,1575],{"class":99},"PaymentResult",[81,1577,113],{"class":87},[81,1579,1580,1583,1586],{"class":83,"line":141},[81,1581,1582],{"class":95},"    paymentClient.",[81,1584,1585],{"class":91},"charge",[81,1587,1588],{"class":95},"(order)\n",[81,1590,1591],{"class":83,"line":159},[81,1592,138],{"emptyLinePlaceholder":137},[81,1594,1595,1597,1600,1602,1604,1607,1610,1612,1614],{"class":83,"line":175},[81,1596,88],{"class":87},[81,1598,1599],{"class":91}," paymentFallback",[81,1601,1567],{"class":95},[81,1603,1570],{"class":99},[81,1605,1606],{"class":95},", ex: ",[81,1608,1609],{"class":99},"Throwable",[81,1611,108],{"class":95},[81,1613,1575],{"class":99},[81,1615,113],{"class":87},[81,1617,1618,1621,1624],{"class":83,"line":199},[81,1619,1620],{"class":95},"    PaymentResult.",[81,1622,1623],{"class":91},"deferred",[81,1625,1588],{"class":95},[12,1627,1628,1629,1632,1633,1636,1637,1640],{},"Le circuit breaker et le retry ne sont que la partie émergée : Resilience4j fournit aussi le ",[25,1630,1631],{},"bulkhead"," (cloisonner les ressources pour qu'une dépendance lente n'asphyxie pas tout le reste), le ",[25,1634,1635],{},"rate limiter"," et le ",[25,1638,1639],{},"time limiter",". Autant de garde-fous que le chaos sert justement à révéler nécessaires : il trouve les points de rupture, Resilience4j les transforme en dégradations contrôlées.",[12,1642,1643,1644,1653,1654,1657,1658,1661,1662,1664],{},"Et pour aller plus loin côté Kotlin, il existe un module dédié, ",[227,1645,1648],{"href":1646,"rel":1647},"https:\u002F\u002Fresilience4j.readme.io\u002Fdocs\u002Fgetting-started-4",[231],[25,1649,1650],{},[78,1651,1652],{},"resilience4j-kotlin",", qui ajoute des extensions pour décorer directement des fonctions ",[78,1655,1656],{},"suspend"," et des ",[78,1659,1660],{},"Flow",". Le circuit breaker, le retry, le rate limiter ou le time limiter s'appliquent alors à du code coroutines sans bloquer un thread : là où la version classique bloque (le temps d'attendre une fenêtre de rate limit, par exemple), l'extension ",[15,1663,1656],{}," à la place.",[71,1666,1668],{"className":73,"code":1667,"language":75,"meta":76,"style":76},"val breaker = circuitBreakerRegistry.circuitBreaker(\"paymentService\")\n\nsuspend fun callPayment(order: Order): PaymentResult =\n    breaker.executeSuspendFunction {\n        paymentClient.charge(order) \u002F\u002F suspend fun\n    }\n",[78,1669,1670,1692,1696,1714,1724,1737],{"__ignoreMap":76},[81,1671,1672,1674,1677,1679,1682,1685,1687,1690],{"class":83,"line":84},[81,1673,850],{"class":87},[81,1675,1676],{"class":95}," breaker ",[81,1678,862],{"class":87},[81,1680,1681],{"class":95}," circuitBreakerRegistry.",[81,1683,1684],{"class":91},"circuitBreaker",[81,1686,165],{"class":95},[81,1688,1689],{"class":168},"\"paymentService\"",[81,1691,876],{"class":95},[81,1693,1694],{"class":83,"line":116},[81,1695,138],{"emptyLinePlaceholder":137},[81,1697,1698,1700,1702,1704,1706,1708,1710,1712],{"class":83,"line":134},[81,1699,1656],{"class":87},[81,1701,1181],{"class":87},[81,1703,1564],{"class":91},[81,1705,1567],{"class":95},[81,1707,1570],{"class":99},[81,1709,108],{"class":95},[81,1711,1575],{"class":99},[81,1713,113],{"class":87},[81,1715,1716,1719,1722],{"class":83,"line":141},[81,1717,1718],{"class":95},"    breaker.",[81,1720,1721],{"class":91},"executeSuspendFunction",[81,1723,1367],{"class":95},[81,1725,1726,1729,1731,1734],{"class":83,"line":159},[81,1727,1728],{"class":95},"        paymentClient.",[81,1730,1585],{"class":91},[81,1732,1733],{"class":95},"(order) ",[81,1735,1736],{"class":885},"\u002F\u002F suspend fun\n",[81,1738,1739],{"class":83,"line":175},[81,1740,202],{"class":95},[12,1742,1743,1744,1746,1747,1749,1750,1753,1754,1757,1758,1763],{},"Deux réflexions pour pousser plus loin que le talk. D'abord, sur ce que Chaos Monkey sait vraiment simuler. Comme ses ",[15,1745,1334],{}," s'injectent dans les beans Spring, dont les ",[78,1748,1341],{}," et les clients HTTP qui font les I\u002FO, ses assauts de latence et d'exception reproduisent très bien les ",[15,1751,1752],{},"symptômes"," d'une base qui rame ou d'un appel réseau qui échoue : c'est exactement ce qu'on veut pour tester comment l'appli réagit à une dépendance lente ou KO. La limite, c'est qu'il simule l'",[25,1755,1756],{},"effet"," à la frontière des beans, pas la panne réseau elle-même. Une vraie perte de paquets, un reset de connexion, une résolution DNS qui traîne, ça se passe sous l'application, là où Chaos Monkey ne voit rien. Pour ce niveau, il faut monter d'un cran : un ",[227,1759,1762],{"href":1760,"rel":1761},"https:\u002F\u002Fgithub.com\u002FShopify\u002Ftoxiproxy",[231],"Toxiproxy"," intercalé entre l'appli et ses dépendances, ou du chaos au niveau du cluster. La couche applicative reste le bon premier pas, parce que c'est celle qu'on contrôle entièrement, sans demander la permission aux ops.",[12,1765,1766,1767,1772],{},"Ensuite, et c'est presque un prérequis : le chaos sans observabilité, c'est juste de la casse. Si je ne peux pas voir comment l'appli encaisse (latences, taux d'erreur, état des circuit breakers), l'expérience ne m'apprend rien. Une ",[227,1768,1771],{"href":1769,"rel":1770},"https:\u002F\u002Fprinciplesofchaos.org\u002F",[231],"expérience de chaos digne de ce nom",", c'est une hypothèse (« si la base ralentit de 2 secondes, l'API répond en mode dégradé »), une mesure, et un verdict. Pas juste débrancher des trucs pour voir ce qui se passe.",[715,1774,1775],{},[12,1776,1777,1778,530,1781,1788],{},"💡 ",[15,1779,1780],{},"Anaïs a également assisté à ce talk lors du DevFest Lyon 2025 et en a fait",[227,1782,1785],{"href":1783,"rel":1784},"https:\u002F\u002Fblog.hoppr.tech\u002Fblogs\u002F2025-12-23-alors-ctait-comment-ce-premier-devfest-lyon-la-suite#pourquoi-adopter-le-chaos-engineering",[231],[15,1786,1787],{},"un retour sur le blog HoppR",[15,1789,1790],{},", avec un regard plus orienté business : une bonne lecture complémentaire.",[715,1792,1793],{},[12,1794,1795,1797,1798,1801],{},[25,1796,721],{}," : c'est la couche la plus facile à négliger parce qu'elle ne se voit pas tant que tout va bien. Or c'est aussi la seule des trois qui teste le système ",[15,1799,1800],{},"complet"," (réseau, dépendances, timeouts) et pas juste mon code. Commencer petit, avec de la latence sur un service en recette, suffit déjà à apprendre beaucoup.",[37,1803],{},[40,1805,1807],{"id":1806},"le-vert-est-une-hypothèse-pas-une-preuve","Le vert est une hypothèse, pas une preuve",[12,1809,1810,1811,1814],{},"Trois talks, trois niveaux, une même idée : le vert qu'on voit dans nos outils est une ",[25,1812,1813],{},"hypothèse de bon fonctionnement",", jamais une preuve. Le mutation testing met à l'épreuve les assertions, l'analyse statique met à l'épreuve le compilateur, le chaos engineering met à l'épreuve la résilience. À chaque étage, on outille le doute plutôt que de le déléguer à la prod.",[12,1816,1817,1818,1821,1822,1825],{},"Et il y a une raison de prendre ça au sérieux maintenant, plus qu'il y a deux ans : une part croissante de notre code n'est plus écrite à la main mais ",[25,1819,1820],{},"générée",". Quand la production de code s'accélère, c'est la capacité à le ",[25,1823,1824],{},"vérifier automatiquement"," qui devient le goulot, et l'avantage compétitif. Le vert ne suffit pas ; reste à se donner les moyens d'en douter intelligemment.",[37,1827],{},[40,1829,1831],{"id":1830},"pour-aller-plus-loin","Pour aller plus loin",[12,1833,1834],{},[25,1835,1836],{},"Les talks DevLille 2026",[1838,1839,1840,1849,1857],"ul",{},[1841,1842,1843,1848],"li",{},[227,1844,1847],{"href":1845,"rel":1846},"https:\u002F\u002Fdevlille.fr\u002Ftalk-page-722a6be6-2a65-404b-ad22-d51315b37e8b\u002F",[231],"Testez vos tests avant qu'ils ne vous trahissent : le mutation testing !",", par Victoire Ladreit de Lacharrière",[1841,1850,1851,1856],{},[227,1852,1855],{"href":1853,"rel":1854},"https:\u002F\u002Fdevlille.fr\u002Ftalk-page-0280f3e5-41d4-499c-9f43-cf7fcd34fd2c\u002F",[231],"Et si vos erreurs se faisaient attraper avant l'exécution ?",", par Remi Taniel",[1841,1858,1859,1864],{},[227,1860,1863],{"href":1861,"rel":1862},"https:\u002F\u002Fdevlille.fr\u002Ftalk-page-e1f48909-47e7-4a70-b5b3-b4e0e7588195\u002F",[231],"Déchaînez le Chaos : tester la résilience de votre application avec Chaos Monkey",", par Erwan Le Tutour",[12,1866,1867],{},[25,1868,1869],{},"Tester ses tests",[1838,1871,1872,1882,1893],{},[1841,1873,1874,1877,1878,1881],{},[227,1875,287],{"href":283,"rel":1876},[231]," : mutation testing sur la JVM, et ",[227,1879,330],{"href":334,"rel":1880},[231]," pour le support Kotlin",[1841,1883,1884,1888,1889,1892],{},[227,1885,1887],{"href":441,"rel":1886},[231],"Kotest – property-based testing"," et ",[227,1890,557],{"href":553,"rel":1891},[231]," côté JUnit 5",[1841,1894,1895,1898],{},[227,1896,712],{"href":710,"rel":1897},[231]," : fuzzing coverage-guided sur la JVM",[12,1900,1901],{},[25,1902,1903],{},"Analyse statique",[1838,1905,1906,1918],{},[1841,1907,1908,187,1911,1888,1914,1917],{},[227,1909,759],{"href":755,"rel":1910},[231],[227,1912,782],{"href":778,"rel":1913},[231],[227,1915,790],{"href":786,"rel":1916},[231]," côté Java",[1841,1919,1920,1923,1924,1928],{},[227,1921,949],{"href":945,"rel":1922},[231]," côté Kotlin (et la ",[227,1925,1927],{"href":837,"rel":1926},[231],"null-safety du langage",")",[12,1930,1931],{},[25,1932,1933],{},"Résilience & chaos",[1838,1935,1936,1947],{},[1841,1937,1938,187,1941,1888,1944],{},[227,1939,1330],{"href":1326,"rel":1940},[231],[227,1942,1515],{"href":1511,"rel":1943},[231],[227,1945,1762],{"href":1760,"rel":1946},[231],[1841,1948,1949],{},[227,1950,1952],{"href":1769,"rel":1951},[231],"Principles of Chaos Engineering",[1023,1954,1955],{},"html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .sPWt5, html code.shiki .sPWt5{--shiki-default:#7EE787}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}",{"title":76,"searchDepth":116,"depth":116,"links":1957},[1958,1962,1963,1964,1965],{"id":42,"depth":116,"text":43,"children":1959},[1960,1961],{"id":402,"depth":134,"text":403},{"id":415,"depth":134,"text":416},{"id":734,"depth":116,"text":735},{"id":1283,"depth":116,"text":1284},{"id":1806,"depth":116,"text":1807},{"id":1830,"depth":116,"text":1831},"2026-06-23T07:27:57.384Z","Retour de DevLille 2026, côté Java\u002FKotlin\u002FSpringboot.  BUILD SUCCESS. Coverage à 98 %. Toute la suite de tests au vert. On connaît tous ce petit shot de dopamine du pipeline tout propre. Et on connaît","md",".\u002Fassets\u002Fcover-image.webp",{"Trois personnages cartoon l'entourent":1971},{"à gauche, un petit monstre vert avec des cornes tenant un drapeau \\\"SURVIVED\\\"":1972},{"représentant les mutants survivants du mutation testing ; au centre-bas, un petit robot bleu avec un point d'interrogation tenant un badge \\\"✅ COMPILES\\\" et l'air perplexe":1973},{"représentant les limites de l'analyse statique ; à droite, un singe malicieux tirant des fils":1974},{"représentant le Chaos Monkey et le chaos engineering":1975,"ogImage":1969,"tags":1977,"published":137,"authors":1984,"reviewers":1991},{" En bas, le titre en gras":1976},"\\\"Le vert ne suffit pas\\\". Fond bleu clair.\"",[1978,454,1979,571,1980,75,1981,1982,1983],"craft","testing","rex","événement","2026","veille tech",[1985],{"id":1986,"name":1987,"image":1988,"linkedin":1989,"x":1990},"33bf4462-cd38-80da-845c-c63b2fd024bf","Florian Hirson",".\u002Fassets\u002Fauthor-florian-hirson.webp","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fflorian-hirson\u002F",null,[1992,1998,2004],{"id":1993,"name":1994,"image":1995,"linkedin":1996,"x":1990,"bio":1997},"67adfd77-4b84-4496-b55d-3391541f59c5","Michaël Bernasinski",".\u002Fassets\u002Freviewer-michael-bernasinski.webp","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fmichael-bernasinski","Principal Lead - Software Engineer - HoppR Lyon",{"id":1999,"name":2000,"image":2001,"linkedin":2002,"x":2003},"02c620f8-3576-4943-b5cf-6117f99220a2","Edouard Cattez",".\u002Fassets\u002Freviewer-edouard-cattez.webp","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fedouard-cattez-865794133\u002F","https:\u002F\u002Fx.com\u002Fecattez",{"id":2005,"name":2006,"image":2007,"linkedin":2008,"x":1990},"37bf4462-cd38-8041-92af-c7e51809adcc","Clement Godet",".\u002Fassets\u002Freviewer-clement-godet.webp","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fcgodet59\u002F","\u002Fblogs\u002F2026-06-23-le-vert-ne-suffit-pas-3-facons-de-douter-de-son-code",false,[],{"title":5,"description":1967},"blogs\u002F2026-06-23-le-vert-ne-suffit-pas-3-facons-de-douter-de-son-code\u002Findex",[],"B_VL6sy5lSDRjIGWfS8TcaWAyqUjB-NKodG3j_0s5dg",[],1782199784054]