[{"data":1,"prerenderedAt":2687},["ShallowReactive",2],{"blog-post-\u002Fblogs\u002F2026-07-28-property-based-testing-comment-bien-choisir-ses-proprietes-avec-kotest":3,"related-\u002Fblogs\u002F2026-07-28-property-based-testing-comment-bien-choisir-ses-proprietes-avec-kotest":2653},{"id":4,"title":5,"alt":6,"authors":7,"body":14,"date":2634,"description":2635,"extension":2636,"image":2637,"meta":2638,"navigation":755,"ogImage":2637,"path":2639,"published":755,"reviewers":2640,"seo":2647,"stem":2648,"tags":2649,"__hash__":2652},"blogs\u002Fblogs\u002F2026-07-28-property-based-testing-comment-bien-choisir-ses-proprietes-avec-kotest\u002Findex.md","Property based testing : comment bien choisir ses propriétés avec Kotest","Illustration isométrique violette sur fond blanc. Un cube central marqué f(x) représente la fonction testée, relié par des connecteurs verts à six tuiles hexagonales disposées en couronne, chacune validée par une coche verte et illustrant un pattern de property based testing : round-trip, idempotence, invariants, oracle, algebraic et easy to verify.",[8],{"id":9,"name":10,"image":11,"linkedin":12,"x":13},"33bf4462-cd38-80da-845c-c63b2fd024bf","Florian Hirson",".\u002Fassets\u002Fauthor-florian-hirson.webp","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fflorian-hirson\u002F",null,{"type":15,"value":16,"toc":2615},"minimark",[17,30,44,47,52,55,115,143,157,168,177,184,187,193,196,200,207,210,250,253,264,267,361,364,366,370,373,378,381,384,387,464,467,473,482,544,554,558,561,603,620,624,627,634,1031,1034,1245,1252,1256,1259,1262,1265,1355,1358,1371,1375,1378,1418,1421,1425,1428,1472,1483,1486,1561,1564,1568,1636,1645,1647,1651,1654,1682,1688,1774,1787,1790,1842,1848,1851,1977,1983,2022,2039,2041,2045,2055,2058,2182,2185,2221,2232,2246,2248,2252,2255,2283,2348,2358,2387,2409,2437,2449,2451,2455,2458,2473,2480,2493,2510,2528,2530,2534,2537,2540,2543,2545,2549,2554,2580,2585,2611],[18,19,20,21,25,26,29],"p",{},"Un test unitaire, tout le monde connait : on choisit une entrée, on vérifie la sortie. ",[22,23,24],"code",{},"[3, 1, 2]"," trié doit rendre ",[22,27,28],{},"[1, 2, 3]",". C'est efficace, mais on ne teste que les cas qu'on a imaginés. Le property based testing résout le problème par une autre approche. On décrit une règle qui doit tenir pour n'importe quelle entrée, et le framework génère des centaines de cas pour tenter de la casser.",[18,31,32,33,43],{},"J'en ai parlé vite dans ",[34,35,39],"a",{"href":36,"rel":37},"https:\u002F\u002Fblog.hoppr.tech\u002Fblogs\u002F2026-06-23-le-vert-ne-suffit-pas-3-facons-de-douter-de-son-code",[38],"nofollow",[40,41,42],"strong",{},"Le vert ne suffit pas : 3 façons de douter de son code",", au milieu d'un REX. C'était assez pour expliquer brièvement le concept, mais trop court pour montrer comment s'en servir. Cet article reprend là où ce REX s'arrête.",[18,45,46],{},"On commence par un rappel express, pour ceux qui n’ont jamais touché au property based testing. Ensuite, on passe à ce qui fait vraiment la différence : trouver la bonne propriété à tester.",[48,49,51],"h2",{"id":50},"le-property-based-testing-en-deux-minutes","Le property based testing en deux minutes",[18,53,54],{},"Concrètement, à quoi ça ressemble ? Voici un exemple : renverser une liste deux fois doit la laisser intacte.",[56,57,62],"pre",{"className":58,"code":59,"language":60,"meta":61,"style":61},"language-kotlin shiki shiki-themes github-dark-default","checkAll(Arb.list(Arb.int())) { list ->\n    list.reversed().reversed() shouldBe list\n}\n","kotlin","",[22,63,64,92,109],{"__ignoreMap":61},[65,66,69,73,77,80,82,85,88],"span",{"class":67,"line":68},"line",1,[65,70,72],{"class":71},"sc3cj","checkAll",[65,74,76],{"class":75},"sZEs4","(Arb.",[65,78,79],{"class":71},"list",[65,81,76],{"class":75},[65,83,84],{"class":71},"int",[65,86,87],{"class":75},"())) { list ",[65,89,91],{"class":90},"suJrU","->\n",[65,93,95,98,101,104,106],{"class":67,"line":94},2,[65,96,97],{"class":75},"    list.",[65,99,100],{"class":71},"reversed",[65,102,103],{"class":75},"().",[65,105,100],{"class":71},[65,107,108],{"class":75},"() shouldBe list\n",[65,110,112],{"class":67,"line":111},3,[65,113,114],{"class":75},"}\n",[18,116,117,118,125,126,130,131,134,135,138,139,142],{},"Un mot sur ",[34,119,122],{"href":120,"rel":121},"https:\u002F\u002Fkotest.io\u002Fdocs\u002Fproptest\u002Fproperty-test-generators.html#arbitrary",[38],[22,123,124],{},"Arb",", puisqu'il revient souvent. C'est le générateur, abréviation d'",[127,128,129],"em",{},"arbitrary",". Il fournit un flux de valeurs d'un type donné : ",[22,132,133],{},"Arb.int()"," débite des entiers, ",[22,136,137],{},"Arb.string()"," des chaînes. Les générateurs se composent, donc ",[22,140,141],{},"Arb.list(Arb.int())"," fabrique des listes d'entiers à partir du générateur d'entiers.",[18,144,145,146,148,149,152,153,156],{},"Un ",[22,147,124],{}," ne tire pas que du hasard. Il mélange des échantillons aléatoires avec des cas limites codés en dur, ceux qui font tomber les tests. Pour les entiers : 0, 1, -1, ",[22,150,151],{},"Int.MIN_VALUE",", ",[22,154,155],{},"Int.MAX_VALUE",". Pour les chaînes, la chaîne vide. Pour les listes, la liste vide ou la liste à un seul élément. Par défaut, environ 2 % des valeurs générées sont ces cas limites, le reste est aléatoire.",[18,158,159,160,167],{},"Kotest a un second type de générateur, ",[34,161,164],{"href":162,"rel":163},"https:\u002F\u002Fkotest.io\u002Fdocs\u002Fproptest\u002Fproperty-test-generators.html#exhaustive",[38],[22,165,166],{},"Exhaustive",", qui parcourt tout un domaine fini plutôt que d'y piocher au hasard. Par exemple un enum ou un booléen. On y revient plus bas.",[18,169,170,171,176],{},"Sur l'exemple ci-dessus, ",[34,172,175],{"href":173,"rel":174},"https:\u002F\u002Fkotest.io\u002F",[38],"Kotest"," génère donc des centaines de listes, des vides comme des énormes, en glissant des valeurs aux limites. Il vérifie la règle sur chacune des valeurs. Si une seule échoue, il la montre.",[18,178,179],{},[180,181],"img",{"alt":182,"src":183},"Cycle d'un property test Kotest : génération, évaluation, verdict, puis shrinking vers un contre-exemple minimal en cas d'échec.","\u002Fcontent-assets\u002F2026-07-28-property-based-testing-comment-bien-choisir-ses-proprietes-avec-kotest\u002Fassets\u002Fimg1.webp",[18,185,186],{},"Là où un test par l'exemple documente un cas précis, la propriété décrit une vérité générale sur tout l'espace d'entrée.",[18,188,189,190,192],{},"Le plus dur arrive après : trouver la propriété qui vaut le coup. Écrire le ",[22,191,72],{}," prend cinq minutes. Savoir quoi affirmer prend beaucoup plus de temps. C'est là que la plupart des tutos s'arrêtent, juste avant la partie intéressante.",[194,195],"hr",{},[48,197,199],{"id":198},"la-vraie-problématique-quelle-propriété-affirmer","La vraie problématique : quelle propriété affirmer ?",[18,201,202,203,206],{},"Une propriété fausse se repère vite : elle échoue. Le piège, c'est la propriété ",[40,204,205],{},"trop faible",". Elle passe au vert et rassure, alors qu'elle ne teste presque rien. C'est exactement le problème du coverage, transposé aux assertions.",[18,208,209],{},"Prenons un exemple, on veut tester une fonction de tri.",[56,211,213],{"className":58,"code":212,"language":60,"meta":61,"style":61},"\u002F\u002F Verte, mais creuse\ncheckAll(Arb.list(Arb.int())) { list ->\n    maFonctionDeTri(list).size shouldBe list.size\n}\n",[22,214,215,221,237,245],{"__ignoreMap":61},[65,216,217],{"class":67,"line":68},[65,218,220],{"class":219},"sH3jZ","\u002F\u002F Verte, mais creuse\n",[65,222,223,225,227,229,231,233,235],{"class":67,"line":94},[65,224,72],{"class":71},[65,226,76],{"class":75},[65,228,79],{"class":71},[65,230,76],{"class":75},[65,232,84],{"class":71},[65,234,87],{"class":75},[65,236,91],{"class":90},[65,238,239,242],{"class":67,"line":111},[65,240,241],{"class":71},"    maFonctionDeTri",[65,243,244],{"class":75},"(list).size shouldBe list.size\n",[65,246,248],{"class":67,"line":247},4,[65,249,114],{"class":75},[18,251,252],{},"Cette propriété vérifie que le tri ne perd pas d'élément. Sauf qu'elle serait tout aussi vérifiée par une implémentation qui renvoie la liste sans y toucher. La propriété ne parle jamais de l'ordre, donc elle ne le protège pas.",[18,254,255,256,263],{},"Scott Wlaschin a un nom pour ce genre d'implémentation, l'",[34,257,260],{"href":258,"rel":259},"https:\u002F\u002Ffsharpforfunandprofit.com\u002Fposts\u002Fproperty-based-testing\u002F",[38],[127,261,262],{},"Enterprise Developer From Hell",", le développeur qui écrit le code le plus bête possible passant quand même les tests. Le personnage donne un réflexe utile, devant chaque propriété se demander quelle implémentation absurde la validerait.",[18,265,266],{},"La version pertinente dit deux choses : le résultat est ordonné et c'est bien une permutation de l'entrée.",[56,268,270],{"className":58,"code":269,"language":60,"meta":61,"style":61},"checkAll(Arb.list(Arb.int())) { list ->\n    val result = maFonctionDeTri(list)\n    result.zipWithNext { a, b -> a \u003C= b }.all { it } shouldBe true\n    result.sorted() shouldBe list.sorted() \u002F\u002F même multiset que l'entrée\n}\n",[22,271,272,288,305,338,356],{"__ignoreMap":61},[65,273,274,276,278,280,282,284,286],{"class":67,"line":68},[65,275,72],{"class":71},[65,277,76],{"class":75},[65,279,79],{"class":71},[65,281,76],{"class":75},[65,283,84],{"class":71},[65,285,87],{"class":75},[65,287,91],{"class":90},[65,289,290,293,296,299,302],{"class":67,"line":94},[65,291,292],{"class":90},"    val",[65,294,295],{"class":75}," result ",[65,297,298],{"class":90},"=",[65,300,301],{"class":71}," maFonctionDeTri",[65,303,304],{"class":75},"(list)\n",[65,306,307,310,313,316,319,322,325,328,331,334],{"class":67,"line":111},[65,308,309],{"class":75},"    result.",[65,311,312],{"class":71},"zipWithNext",[65,314,315],{"class":75}," { a, b ",[65,317,318],{"class":90},"->",[65,320,321],{"class":75}," a ",[65,323,324],{"class":90},"\u003C=",[65,326,327],{"class":75}," b }.",[65,329,330],{"class":71},"all",[65,332,333],{"class":75}," { it } shouldBe ",[65,335,337],{"class":336},"sFSAA","true\n",[65,339,340,342,345,348,350,353],{"class":67,"line":247},[65,341,309],{"class":75},[65,343,344],{"class":71},"sorted",[65,346,347],{"class":75},"() shouldBe list.",[65,349,344],{"class":71},[65,351,352],{"class":75},"() ",[65,354,355],{"class":219},"\u002F\u002F même multiset que l'entrée\n",[65,357,359],{"class":67,"line":358},5,[65,360,114],{"class":75},[18,362,363],{},"Ce saut demande de comprendre le problème, pas de connaître une méthode Kotest de plus. Heureusement il existe une poignée de patterns utiles pour cela. Une fois qu'on les a en tête, on ne fixe plus son code en se demandant quoi tester, on déroule.",[194,365],{},[48,367,369],{"id":368},"six-patterns-pour-trouver-des-propriétés","Six patterns pour trouver des propriétés",[18,371,372],{},"Voici une boîte à outils mentale: face à une fonction, on passe les six en revue et on garde celles qui correspondent le plus.",[374,375,377],"h3",{"id":376},"_1-aller-retour-round-trip","1. Aller-retour (round-trip)",[18,379,380],{},"Si une opération a un inverse, les enchaîner doit ramener au point de départ. Sérialiser puis désérialiser doit redonner l'objet d'origine. Pareil pour encoder puis décoder, ou sauvegarder puis relire.",[18,382,383],{},"C'est là que je brancherais un property test en premier dans ma stack actuelle, je pense à deux sujets en particulier.",[18,385,386],{},"Commençons par la sérialisation: je migre de Jackson 2 vers Jackson 3 en ce moment. La propriété qui sécurise toute la migration tient en trois lignes :",[56,388,390],{"className":58,"code":389,"language":60,"meta":61,"style":61},"test(\"un round-trip JSON préserve l'objet\") {\n    checkAll(orderArb) { order ->\n        val json = mapper.writeValueAsString(order)\n        mapper.readValue\u003COrder>(json) shouldBe order\n    }\n}\n",[22,391,392,407,417,436,454,459],{"__ignoreMap":61},[65,393,394,397,400,404],{"class":67,"line":68},[65,395,396],{"class":71},"test",[65,398,399],{"class":75},"(",[65,401,403],{"class":402},"s9uIt","\"un round-trip JSON préserve l'objet\"",[65,405,406],{"class":75},") {\n",[65,408,409,412,415],{"class":67,"line":94},[65,410,411],{"class":71},"    checkAll",[65,413,414],{"class":75},"(orderArb) { order ",[65,416,91],{"class":90},[65,418,419,422,425,427,430,433],{"class":67,"line":111},[65,420,421],{"class":90},"        val",[65,423,424],{"class":75}," json ",[65,426,298],{"class":90},[65,428,429],{"class":75}," mapper.",[65,431,432],{"class":71},"writeValueAsString",[65,434,435],{"class":75},"(order)\n",[65,437,438,441,444,447,451],{"class":67,"line":247},[65,439,440],{"class":75},"        mapper.",[65,442,443],{"class":71},"readValue",[65,445,446],{"class":75},"\u003C",[65,448,450],{"class":449},"sQhOw","Order",[65,452,453],{"class":75},">(json) shouldBe order\n",[65,455,456],{"class":67,"line":358},[65,457,458],{"class":75},"    }\n",[65,460,462],{"class":67,"line":461},6,[65,463,114],{"class":75},[18,465,466],{},"Une régression de config, un champ qui part à la sérialisation mais ne revient pas au parsing : le test échoue. Et il échoue sur des centaines de valeurs que je n'aurais jamais définis manuellement.",[18,468,469],{},[180,470],{"alt":471,"src":472},"Diagramme de séquence du round-trip : un Order généré est sérialisé en JSON par Jackson, désérialisé, puis comparé à l'original.","\u002Fcontent-assets\u002F2026-07-28-property-based-testing-comment-bien-choisir-ses-proprietes-avec-kotest\u002Fassets\u002Fimg2.webp",[18,474,475,476,481],{},"Vient ensuite le JSON Patch. J'écris des tests de conformité ",[34,477,480],{"href":478,"rel":479},"https:\u002F\u002Fdatatracker.ietf.org\u002Fdoc\u002Fhtml\u002Frfc6902",[38],"RFC 6902",". Cette specification est un terrain de jeu à round-trips. Voici la propriété :",[56,483,485],{"className":58,"code":484,"language":60,"meta":61,"style":61},"test(\"appliquer diff(a, b) à a redonne b\") {\n    checkAll(jsonArb, jsonArb) { a, b ->\n        val patch = JsonDiff.asJson(a, b)\n        JsonPatch.apply(patch, a) shouldBe b\n    }\n}\n",[22,486,487,498,507,525,536,540],{"__ignoreMap":61},[65,488,489,491,493,496],{"class":67,"line":68},[65,490,396],{"class":71},[65,492,399],{"class":75},[65,494,495],{"class":402},"\"appliquer diff(a, b) à a redonne b\"",[65,497,406],{"class":75},[65,499,500,502,505],{"class":67,"line":94},[65,501,411],{"class":71},[65,503,504],{"class":75},"(jsonArb, jsonArb) { a, b ",[65,506,91],{"class":90},[65,508,509,511,514,516,519,522],{"class":67,"line":111},[65,510,421],{"class":90},[65,512,513],{"class":75}," patch ",[65,515,298],{"class":90},[65,517,518],{"class":75}," JsonDiff.",[65,520,521],{"class":71},"asJson",[65,523,524],{"class":75},"(a, b)\n",[65,526,527,530,533],{"class":67,"line":247},[65,528,529],{"class":75},"        JsonPatch.",[65,531,532],{"class":71},"apply",[65,534,535],{"class":75},"(patch, a) shouldBe b\n",[65,537,538],{"class":67,"line":358},[65,539,458],{"class":75},[65,541,542],{"class":67,"line":461},[65,543,114],{"class":75},[18,545,546,547,550,551,553],{},"Si ",[22,548,549],{},"diff"," et ",[22,552,532],{}," sont cohérents, ça tient pour n'importe quel couple de documents. Si un jour ça casse, le property based testing me sort le plus petit couple qui met en défaut ma conformité. Aucun test par l'exemple ne couvre ça avec cette densité.",[374,555,557],{"id":556},"_2-idempotence","2. Idempotence",[18,559,560],{},"Appliquer une opération une fois ou dix fois donne le même résultat. On le retrouve sur les normalisations et les déduplications, ou sur un simple tri.",[56,562,564],{"className":58,"code":563,"language":60,"meta":61,"style":61},"checkAll(Arb.list(Arb.int())) { list ->\n    list.sorted().sorted() shouldBe list.sorted()\n}\n",[22,565,566,582,599],{"__ignoreMap":61},[65,567,568,570,572,574,576,578,580],{"class":67,"line":68},[65,569,72],{"class":71},[65,571,76],{"class":75},[65,573,79],{"class":71},[65,575,76],{"class":75},[65,577,84],{"class":71},[65,579,87],{"class":75},[65,581,91],{"class":90},[65,583,584,586,588,590,592,594,596],{"class":67,"line":94},[65,585,97],{"class":75},[65,587,344],{"class":71},[65,589,103],{"class":75},[65,591,344],{"class":71},[65,593,347],{"class":75},[65,595,344],{"class":71},[65,597,598],{"class":75},"()\n",[65,600,601],{"class":67,"line":111},[65,602,114],{"class":75},[18,604,605,606,152,609,152,612,615,616,619],{},"Dès qu'on écrit une fonction ",[22,607,608],{},"normalize",[22,610,611],{},"sanitize",[22,613,614],{},"dedupe"," ou ",[22,617,618],{},"canonicalize",", on se pose la question. Dans la plupart des cas, elle doit être idempotente.",[374,621,623],{"id":622},"_3-invariants","3. Invariants",[18,625,626],{},"Certaines choses ne changent jamais, quoi que fasse le code. Une permutation garde la même taille et les mêmes éléments. Une conversion de devise conserve le total, à l'arrondi près. Et l'identité d'un objet transformé ne doit pas changer.",[18,628,629,630,633],{},"C'est un pattern puissant sur du code métier, parce qu'il permet de tester une transformation ",[40,631,632],{},"sans figer sa sortie exacte",". Prenons un cas réaliste : un moteur qui assouplit des critères de recherche quand une requête ne remonte pas assez de résultats.",[56,635,637],{"className":58,"code":636,"language":60,"meta":61,"style":61},"data class SearchFilter(\n    val sessionId: String,\n    val keywords: List\u003CString>,\n    val startDate: LocalDate?,\n    val endDate: LocalDate?,\n    val location: Coordinates?,\n    val maxPrice: BigDecimal?,\n    val minRating: BigDecimal?,\n)\n\nenum class RelaxStrategy(val modifier: (SearchFilter) -> SearchFilter) {\n    WIDEN_MAX_PRICE({ it.copy(maxPrice = it.maxPrice?.multiply(BigDecimal(\"1.25\"))) }),\n    LOWER_MIN_RATING({ it.copy(minRating = it.minRating?.multiply(BigDecimal(\"0.8\"))) }),\n    SHIFT_DATES_LATER({ f ->\n        if (f.startDate != null && f.endDate != null) {\n            f.copy(startDate = f.startDate.plusDays(14), endDate = f.endDate.plusDays(14))\n        } else {\n            f\n        }\n    }),\n    DROP_LOCATION({ it.copy(location = null) }),\n    ;\n\n    fun shouldExecute(filter: SearchFilter): Boolean = modifier(filter) != filter\n}\n",[22,638,639,653,666,683,696,707,719,732,744,750,757,786,821,852,863,890,931,943,949,955,961,981,987,992,1026],{"__ignoreMap":61},[65,640,641,644,647,650],{"class":67,"line":68},[65,642,643],{"class":90},"data",[65,645,646],{"class":90}," class",[65,648,649],{"class":449}," SearchFilter",[65,651,652],{"class":75},"(\n",[65,654,655,657,660,663],{"class":67,"line":94},[65,656,292],{"class":90},[65,658,659],{"class":75}," sessionId: ",[65,661,662],{"class":449},"String",[65,664,665],{"class":75},",\n",[65,667,668,670,673,676,678,680],{"class":67,"line":111},[65,669,292],{"class":90},[65,671,672],{"class":75}," keywords: ",[65,674,675],{"class":449},"List",[65,677,446],{"class":75},[65,679,662],{"class":449},[65,681,682],{"class":75},">,\n",[65,684,685,687,690,693],{"class":67,"line":247},[65,686,292],{"class":90},[65,688,689],{"class":75}," startDate: ",[65,691,692],{"class":449},"LocalDate",[65,694,695],{"class":75},"?,\n",[65,697,698,700,703,705],{"class":67,"line":358},[65,699,292],{"class":90},[65,701,702],{"class":75}," endDate: ",[65,704,692],{"class":449},[65,706,695],{"class":75},[65,708,709,711,714,717],{"class":67,"line":461},[65,710,292],{"class":90},[65,712,713],{"class":75}," location: ",[65,715,716],{"class":449},"Coordinates",[65,718,695],{"class":75},[65,720,722,724,727,730],{"class":67,"line":721},7,[65,723,292],{"class":90},[65,725,726],{"class":75}," maxPrice: ",[65,728,729],{"class":449},"BigDecimal",[65,731,695],{"class":75},[65,733,735,737,740,742],{"class":67,"line":734},8,[65,736,292],{"class":90},[65,738,739],{"class":75}," minRating: ",[65,741,729],{"class":449},[65,743,695],{"class":75},[65,745,747],{"class":67,"line":746},9,[65,748,749],{"class":75},")\n",[65,751,753],{"class":67,"line":752},10,[65,754,756],{"emptyLinePlaceholder":755},true,"\n",[65,758,760,763,765,768,770,773,776,779,782,784],{"class":67,"line":759},11,[65,761,762],{"class":90},"enum",[65,764,646],{"class":90},[65,766,767],{"class":449}," RelaxStrategy",[65,769,399],{"class":75},[65,771,772],{"class":90},"val",[65,774,775],{"class":75}," modifier: (",[65,777,778],{"class":449},"SearchFilter",[65,780,781],{"class":75},") -> ",[65,783,778],{"class":449},[65,785,406],{"class":75},[65,787,789,792,795,798,801,803,806,809,811,813,815,818],{"class":67,"line":788},12,[65,790,791],{"class":71},"    WIDEN_MAX_PRICE",[65,793,794],{"class":75},"({ it.",[65,796,797],{"class":71},"copy",[65,799,800],{"class":75},"(maxPrice ",[65,802,298],{"class":90},[65,804,805],{"class":75}," it.maxPrice?.",[65,807,808],{"class":71},"multiply",[65,810,399],{"class":75},[65,812,729],{"class":71},[65,814,399],{"class":75},[65,816,817],{"class":402},"\"1.25\"",[65,819,820],{"class":75},"))) }),\n",[65,822,824,827,829,831,834,836,839,841,843,845,847,850],{"class":67,"line":823},13,[65,825,826],{"class":71},"    LOWER_MIN_RATING",[65,828,794],{"class":75},[65,830,797],{"class":71},[65,832,833],{"class":75},"(minRating ",[65,835,298],{"class":90},[65,837,838],{"class":75}," it.minRating?.",[65,840,808],{"class":71},[65,842,399],{"class":75},[65,844,729],{"class":71},[65,846,399],{"class":75},[65,848,849],{"class":402},"\"0.8\"",[65,851,820],{"class":75},[65,853,855,858,861],{"class":67,"line":854},14,[65,856,857],{"class":71},"    SHIFT_DATES_LATER",[65,859,860],{"class":75},"({ f ",[65,862,91],{"class":90},[65,864,866,869,872,875,878,881,884,886,888],{"class":67,"line":865},15,[65,867,868],{"class":90},"        if",[65,870,871],{"class":75}," (f.startDate ",[65,873,874],{"class":90},"!=",[65,876,877],{"class":336}," null",[65,879,880],{"class":90}," &&",[65,882,883],{"class":75}," f.endDate ",[65,885,874],{"class":90},[65,887,877],{"class":336},[65,889,406],{"class":75},[65,891,893,896,898,901,903,906,909,911,914,917,919,922,924,926,928],{"class":67,"line":892},16,[65,894,895],{"class":75},"            f.",[65,897,797],{"class":71},[65,899,900],{"class":75},"(startDate ",[65,902,298],{"class":90},[65,904,905],{"class":75}," f.startDate.",[65,907,908],{"class":71},"plusDays",[65,910,399],{"class":75},[65,912,913],{"class":336},"14",[65,915,916],{"class":75},"), endDate ",[65,918,298],{"class":90},[65,920,921],{"class":75}," f.endDate.",[65,923,908],{"class":71},[65,925,399],{"class":75},[65,927,913],{"class":336},[65,929,930],{"class":75},"))\n",[65,932,934,937,940],{"class":67,"line":933},17,[65,935,936],{"class":75},"        } ",[65,938,939],{"class":90},"else",[65,941,942],{"class":75}," {\n",[65,944,946],{"class":67,"line":945},18,[65,947,948],{"class":75},"            f\n",[65,950,952],{"class":67,"line":951},19,[65,953,954],{"class":75},"        }\n",[65,956,958],{"class":67,"line":957},20,[65,959,960],{"class":75},"    }),\n",[65,962,964,967,969,971,974,976,978],{"class":67,"line":963},21,[65,965,966],{"class":71},"    DROP_LOCATION",[65,968,794],{"class":75},[65,970,797],{"class":71},[65,972,973],{"class":75},"(location ",[65,975,298],{"class":90},[65,977,877],{"class":336},[65,979,980],{"class":75},") }),\n",[65,982,984],{"class":67,"line":983},22,[65,985,986],{"class":75},"    ;\n",[65,988,990],{"class":67,"line":989},23,[65,991,756],{"emptyLinePlaceholder":755},[65,993,995,998,1001,1004,1006,1009,1012,1015,1018,1021,1023],{"class":67,"line":994},24,[65,996,997],{"class":90},"    fun",[65,999,1000],{"class":71}," shouldExecute",[65,1002,1003],{"class":75},"(filter: ",[65,1005,778],{"class":449},[65,1007,1008],{"class":75},"): ",[65,1010,1011],{"class":449},"Boolean",[65,1013,1014],{"class":90}," =",[65,1016,1017],{"class":71}," modifier",[65,1019,1020],{"class":75},"(filter) ",[65,1022,874],{"class":90},[65,1024,1025],{"class":75}," filter\n",[65,1027,1029],{"class":67,"line":1028},25,[65,1030,114],{"class":75},[18,1032,1033],{},"Chaque stratégie relâche une contrainte différente. Les tester une par une par l'exemple, c'est un test par stratégie qui rate les combinaisons. La propriété couvre toutes les stratégies sur tout l'espace d'entrée d'un coup, en n'affirmant que ce qui doit rester vrai :",[56,1035,1037],{"className":58,"code":1036,"language":60,"meta":61,"style":61},"test(\"relâcher un filtre préserve ses invariants\") {\n    checkAll(searchFilterArb, Arb.enum\u003CRelaxStrategy>()) { filter, strategy ->\n        val relaxed = strategy.modifier(filter)\n\n        \u002F\u002F l'identité de session ne bouge jamais\n        relaxed.sessionId shouldBe filter.sessionId\n\n        \u002F\u002F les montants restent positifs\n        relaxed.maxPrice?.signum()?.let { it shouldBeGreaterThanOrEqual 0 }\n        relaxed.minRating?.signum()?.let { it shouldBeGreaterThanOrEqual 0 }\n\n        \u002F\u002F l'ordre des dates tient toujours\n        if (relaxed.startDate != null && relaxed.endDate != null) {\n            (relaxed.startDate!! \u003C= relaxed.endDate!!) shouldBe true\n        }\n\n        \u002F\u002F une stratégie censée s'exécuter doit changer au moins un champ\n        if (strategy.shouldExecute(filter)) {\n            relaxed shouldNotBe filter\n        }\n    }\n}\n",[22,1038,1039,1050,1069,1087,1091,1096,1101,1105,1110,1133,1150,1154,1159,1181,1202,1206,1210,1215,1228,1233,1237,1241],{"__ignoreMap":61},[65,1040,1041,1043,1045,1048],{"class":67,"line":68},[65,1042,396],{"class":71},[65,1044,399],{"class":75},[65,1046,1047],{"class":402},"\"relâcher un filtre préserve ses invariants\"",[65,1049,406],{"class":75},[65,1051,1052,1054,1057,1059,1061,1064,1067],{"class":67,"line":94},[65,1053,411],{"class":71},[65,1055,1056],{"class":75},"(searchFilterArb, Arb.",[65,1058,762],{"class":71},[65,1060,446],{"class":75},[65,1062,1063],{"class":449},"RelaxStrategy",[65,1065,1066],{"class":75},">()) { filter, strategy ",[65,1068,91],{"class":90},[65,1070,1071,1073,1076,1078,1081,1084],{"class":67,"line":111},[65,1072,421],{"class":90},[65,1074,1075],{"class":75}," relaxed ",[65,1077,298],{"class":90},[65,1079,1080],{"class":75}," strategy.",[65,1082,1083],{"class":71},"modifier",[65,1085,1086],{"class":75},"(filter)\n",[65,1088,1089],{"class":67,"line":247},[65,1090,756],{"emptyLinePlaceholder":755},[65,1092,1093],{"class":67,"line":358},[65,1094,1095],{"class":219},"        \u002F\u002F l'identité de session ne bouge jamais\n",[65,1097,1098],{"class":67,"line":461},[65,1099,1100],{"class":75},"        relaxed.sessionId shouldBe filter.sessionId\n",[65,1102,1103],{"class":67,"line":721},[65,1104,756],{"emptyLinePlaceholder":755},[65,1106,1107],{"class":67,"line":734},[65,1108,1109],{"class":219},"        \u002F\u002F les montants restent positifs\n",[65,1111,1112,1115,1118,1121,1124,1127,1130],{"class":67,"line":746},[65,1113,1114],{"class":75},"        relaxed.maxPrice?.",[65,1116,1117],{"class":71},"signum",[65,1119,1120],{"class":75},"()?.",[65,1122,1123],{"class":71},"let",[65,1125,1126],{"class":75}," { it shouldBeGreaterThanOrEqual ",[65,1128,1129],{"class":336},"0",[65,1131,1132],{"class":75}," }\n",[65,1134,1135,1138,1140,1142,1144,1146,1148],{"class":67,"line":752},[65,1136,1137],{"class":75},"        relaxed.minRating?.",[65,1139,1117],{"class":71},[65,1141,1120],{"class":75},[65,1143,1123],{"class":71},[65,1145,1126],{"class":75},[65,1147,1129],{"class":336},[65,1149,1132],{"class":75},[65,1151,1152],{"class":67,"line":759},[65,1153,756],{"emptyLinePlaceholder":755},[65,1155,1156],{"class":67,"line":788},[65,1157,1158],{"class":219},"        \u002F\u002F l'ordre des dates tient toujours\n",[65,1160,1161,1163,1166,1168,1170,1172,1175,1177,1179],{"class":67,"line":823},[65,1162,868],{"class":90},[65,1164,1165],{"class":75}," (relaxed.startDate ",[65,1167,874],{"class":90},[65,1169,877],{"class":336},[65,1171,880],{"class":90},[65,1173,1174],{"class":75}," relaxed.endDate ",[65,1176,874],{"class":90},[65,1178,877],{"class":336},[65,1180,406],{"class":75},[65,1182,1183,1186,1189,1192,1195,1197,1200],{"class":67,"line":854},[65,1184,1185],{"class":75},"            (relaxed.startDate",[65,1187,1188],{"class":90},"!!",[65,1190,1191],{"class":90}," \u003C=",[65,1193,1194],{"class":75}," relaxed.endDate",[65,1196,1188],{"class":90},[65,1198,1199],{"class":75},") shouldBe ",[65,1201,337],{"class":336},[65,1203,1204],{"class":67,"line":865},[65,1205,954],{"class":75},[65,1207,1208],{"class":67,"line":892},[65,1209,756],{"emptyLinePlaceholder":755},[65,1211,1212],{"class":67,"line":933},[65,1213,1214],{"class":219},"        \u002F\u002F une stratégie censée s'exécuter doit changer au moins un champ\n",[65,1216,1217,1219,1222,1225],{"class":67,"line":945},[65,1218,868],{"class":90},[65,1220,1221],{"class":75}," (strategy.",[65,1223,1224],{"class":71},"shouldExecute",[65,1226,1227],{"class":75},"(filter)) {\n",[65,1229,1230],{"class":67,"line":951},[65,1231,1232],{"class":75},"            relaxed shouldNotBe filter\n",[65,1234,1235],{"class":67,"line":957},[65,1236,954],{"class":75},[65,1238,1239],{"class":67,"line":963},[65,1240,458],{"class":75},[65,1242,1243],{"class":67,"line":983},[65,1244,114],{"class":75},[18,1246,1247,1248,1251],{},"Notons la différence de nature avec un test par l'exemple. Le test unitaire épingle une valeur exacte, celle où ",[22,1249,1250],{},"WIDEN_MAX_PRICE"," multiplie par 1,25. La propriété se contente d'affirmer que les invariants tiennent. Elle est donc plus souple, sans être moins utile. Les deux répondent à des questions différentes. On verra plus tard pourquoi les deux sont à garder.",[374,1253,1255],{"id":1254},"_4-dur-à-calculer-facile-à-vérifier","4. Dur à calculer, facile à vérifier",[18,1257,1258],{},"Parfois il n'y a ni inverse, ni implémentation de référence. Dans ce cas là je me pose la question : est-ce que je sais reconnaître une bonne sortie quand j'en vois une ? Souvent oui, pour bien moins cher que de la calculer.",[18,1260,1261],{},"C'est déjà ce qu'on a fait plus haut sur le tri. Vérifier que chaque élément est plus petit que le suivant demande une boucle, alors qu'écrire un tri correct demande un algorithme.",[18,1263,1264],{},"Le cas où ce pattern devient utile, c'est quand la sortie est trop compliquée pour être figée. Prenons un traitement topologique, celui qui ordonne des dépendances hiérarchiques. Calculer l'ordre est le vrai travail. Vérifier qu'il tient se résume à deux assertions.",[56,1266,1268],{"className":58,"code":1267,"language":60,"meta":61,"style":61},"test(\"l'ordre produit respecte toutes les dépendances\") {\n    checkAll(dagArb) { graph ->\n        val ordered = topologicalSort(graph)\n\n        ordered shouldContainExactlyInAnyOrder graph.nodes\n        graph.edges.forEach { (from, to) ->\n            ordered.indexOf(from) shouldBeLessThan ordered.indexOf(to)\n        }\n    }\n}\n",[22,1269,1270,1281,1290,1305,1309,1314,1327,1343,1347,1351],{"__ignoreMap":61},[65,1271,1272,1274,1276,1279],{"class":67,"line":68},[65,1273,396],{"class":71},[65,1275,399],{"class":75},[65,1277,1278],{"class":402},"\"l'ordre produit respecte toutes les dépendances\"",[65,1280,406],{"class":75},[65,1282,1283,1285,1288],{"class":67,"line":94},[65,1284,411],{"class":71},[65,1286,1287],{"class":75},"(dagArb) { graph ",[65,1289,91],{"class":90},[65,1291,1292,1294,1297,1299,1302],{"class":67,"line":111},[65,1293,421],{"class":90},[65,1295,1296],{"class":75}," ordered ",[65,1298,298],{"class":90},[65,1300,1301],{"class":71}," topologicalSort",[65,1303,1304],{"class":75},"(graph)\n",[65,1306,1307],{"class":67,"line":247},[65,1308,756],{"emptyLinePlaceholder":755},[65,1310,1311],{"class":67,"line":358},[65,1312,1313],{"class":75},"        ordered shouldContainExactlyInAnyOrder graph.nodes\n",[65,1315,1316,1319,1322,1325],{"class":67,"line":461},[65,1317,1318],{"class":75},"        graph.edges.",[65,1320,1321],{"class":71},"forEach",[65,1323,1324],{"class":75}," { (from, to) ",[65,1326,91],{"class":90},[65,1328,1329,1332,1335,1338,1340],{"class":67,"line":721},[65,1330,1331],{"class":75},"            ordered.",[65,1333,1334],{"class":71},"indexOf",[65,1336,1337],{"class":75},"(from) shouldBeLessThan ordered.",[65,1339,1334],{"class":71},[65,1341,1342],{"class":75},"(to)\n",[65,1344,1345],{"class":67,"line":734},[65,1346,954],{"class":75},[65,1348,1349],{"class":67,"line":746},[65,1350,458],{"class":75},[65,1352,1353],{"class":67,"line":752},[65,1354,114],{"class":75},[18,1356,1357],{},"Notons la différence avec l'oracle de la section suivante. Là-bas j'ai besoin de l'ancienne implémentation pour comparer. Ici je n'ai besoin d'aucune des deux, donc la propriété survit au jour où l'ancienne version disparaît du dépôt.",[18,1359,1360,1361,1366,1367,1370],{},"Ce pattern est aussi le seul qui tienne quand la sortie n'est pas déterministe. Sur de la ",[34,1362,1365],{"href":1363,"rel":1364},"https:\u002F\u002Fblog.hoppr.tech\u002Fblogs\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide",[38],"génération de requêtes par LLM",", deux appels avec la même entrée ne rendent pas le même texte. Impossible de figer une valeur ou de comparer à une référence. Par contre, vérifier que la requête parse et qu'elle ne contient aucune clause d'écriture reste trivial. Attention au coût cela dit, une propriété qui appelle un modèle à chaque itération se règle avec un ",[22,1368,1369],{},"iterations"," très bas.",[374,1372,1374],{"id":1373},"_5-oracle","5. Oracle",[18,1376,1377],{},"On a deux implémentations censées faire la même chose ? On les compare. L’implémentation lente et évidente sert de référence à celle rapide et complexe.",[56,1379,1381],{"className":58,"code":1380,"language":60,"meta":61,"style":61},"checkAll(Arb.list(Arb.int())) { list ->\n    monTriMaison(list) shouldBe list.sorted() \u002F\u002F sorted() est l'oracle\n}\n",[22,1382,1383,1399,1414],{"__ignoreMap":61},[65,1384,1385,1387,1389,1391,1393,1395,1397],{"class":67,"line":68},[65,1386,72],{"class":71},[65,1388,76],{"class":75},[65,1390,79],{"class":71},[65,1392,76],{"class":75},[65,1394,84],{"class":71},[65,1396,87],{"class":75},[65,1398,91],{"class":90},[65,1400,1401,1404,1407,1409,1411],{"class":67,"line":94},[65,1402,1403],{"class":71},"    monTriMaison",[65,1405,1406],{"class":75},"(list) shouldBe list.",[65,1408,344],{"class":71},[65,1410,352],{"class":75},[65,1412,1413],{"class":219},"\u002F\u002F sorted() est l'oracle\n",[65,1415,1416],{"class":67,"line":111},[65,1417,114],{"class":75},[18,1419,1420],{},"Ce pattern est super utile pour un refacto à iso-comportement. Quand j'ai retravaillé un traitement topologique niveau par niveau, la propriété qui m'aurait le plus rassuré, c'est \"nouvelle implémentation égale ancienne, sur des milliers de graphes générés\". L'ancienne version, même moche, fait un oracle parfait le temps de la bascule.",[374,1422,1424],{"id":1423},"_6-propriétés-algébriques","6. Propriétés algébriques",[18,1426,1427],{},"Prenons la commutativité ou l'associativité. Elles sont dures à prouver sur papier et triviales à vérifier sur des centaines de cas.",[56,1429,1431],{"className":58,"code":1430,"language":60,"meta":61,"style":61},"checkAll(Arb.int(), Arb.int()) { a, b ->\n    max(a, b) shouldBe max(b, a) \u002F\u002F commutatif\n}\n",[22,1432,1433,1451,1468],{"__ignoreMap":61},[65,1434,1435,1437,1439,1441,1444,1446,1449],{"class":67,"line":68},[65,1436,72],{"class":71},[65,1438,76],{"class":75},[65,1440,84],{"class":71},[65,1442,1443],{"class":75},"(), Arb.",[65,1445,84],{"class":71},[65,1447,1448],{"class":75},"()) { a, b ",[65,1450,91],{"class":90},[65,1452,1453,1456,1459,1462,1465],{"class":67,"line":94},[65,1454,1455],{"class":71},"    max",[65,1457,1458],{"class":75},"(a, b) shouldBe ",[65,1460,1461],{"class":71},"max",[65,1463,1464],{"class":75},"(b, a) ",[65,1466,1467],{"class":219},"\u002F\u002F commutatif\n",[65,1469,1470],{"class":67,"line":111},[65,1471,114],{"class":75},[18,1473,1474,1475,1478,1479,1482],{},"C'est utile dès qu'on écrit une opération de fusion, un ",[22,1476,1477],{},"merge"," de config par exemple. Si ce ",[22,1480,1481],{},"merge(a, b)"," doit être commutatif, une propriété le prouve mieux que trois exemples posés à la main.",[18,1484,1485],{},"Il existe une forme plus large de ce pattern. Au lieu de permuter les arguments d'une opération, on compare deux chemins qui doivent arriver au même endroit. Reprenons les stratégies de relaxation vues plus haut. Chacune touche un champ différent, donc les appliquer dans un ordre ou dans l'autre doit donner le même filtre.",[56,1487,1489],{"className":58,"code":1488,"language":60,"meta":61,"style":61},"test(\"l'ordre d'application des stratégies n'a pas d'effet\") {\n    checkAll(searchFilterArb, Arb.enum\u003CRelaxStrategy>(), Arb.enum\u003CRelaxStrategy>()) { filter, s1, s2 ->\n        s2.modifier(s1.modifier(filter)) shouldBe s1.modifier(s2.modifier(filter))\n    }\n}\n",[22,1490,1491,1502,1528,1553,1557],{"__ignoreMap":61},[65,1492,1493,1495,1497,1500],{"class":67,"line":68},[65,1494,396],{"class":71},[65,1496,399],{"class":75},[65,1498,1499],{"class":402},"\"l'ordre d'application des stratégies n'a pas d'effet\"",[65,1501,406],{"class":75},[65,1503,1504,1506,1508,1510,1512,1514,1517,1519,1521,1523,1526],{"class":67,"line":94},[65,1505,411],{"class":71},[65,1507,1056],{"class":75},[65,1509,762],{"class":71},[65,1511,446],{"class":75},[65,1513,1063],{"class":449},[65,1515,1516],{"class":75},">(), Arb.",[65,1518,762],{"class":71},[65,1520,446],{"class":75},[65,1522,1063],{"class":449},[65,1524,1525],{"class":75},">()) { filter, s1, s2 ",[65,1527,91],{"class":90},[65,1529,1530,1533,1535,1538,1540,1543,1545,1548,1550],{"class":67,"line":111},[65,1531,1532],{"class":75},"        s2.",[65,1534,1083],{"class":71},[65,1536,1537],{"class":75},"(s1.",[65,1539,1083],{"class":71},[65,1541,1542],{"class":75},"(filter)) shouldBe s1.",[65,1544,1083],{"class":71},[65,1546,1547],{"class":75},"(s2.",[65,1549,1083],{"class":71},[65,1551,1552],{"class":75},"(filter))\n",[65,1554,1555],{"class":67,"line":247},[65,1556,458],{"class":75},[65,1558,1559],{"class":67,"line":358},[65,1560,114],{"class":75},[18,1562,1563],{},"Cette propriété dit quelque chose de fort sur le moteur : il peut relâcher les contraintes dans n'importe quel ordre. Le jour où quelqu'un ajoute une stratégie qui touche un champ déjà modifié par une autre, le test tombe et pose la bonne question.",[374,1565,1567],{"id":1566},"le-tableau-à-garder-sous-le-coude","Le tableau à garder sous le coude",[1569,1570,1571,1584],"table",{},[1572,1573,1574],"thead",{},[1575,1576,1577,1581],"tr",{},[1578,1579,1580],"th",{},"Le code ressemble à",[1578,1582,1583],{},"Pattern à utiliser",[1585,1586,1587,1596,1604,1612,1620,1628],"tbody",{},[1575,1588,1589,1593],{},[1590,1591,1592],"td",{},"encode \u002F decode, save \u002F load, serialize",[1590,1594,1595],{},"Aller-retour",[1575,1597,1598,1601],{},[1590,1599,1600],{},"normalize, sanitize, dedupe, sort",[1590,1602,1603],{},"Idempotence",[1575,1605,1606,1609],{},[1590,1607,1608],{},"une transformation métier",[1590,1610,1611],{},"Invariants",[1575,1613,1614,1617],{},[1590,1615,1616],{},"un ordonnancement, une sortie non déterministe",[1590,1618,1619],{},"Dur à calculer, facile à vérifier",[1575,1621,1622,1625],{},[1590,1623,1624],{},"un refacto, une version optimisée",[1590,1626,1627],{},"Oracle",[1575,1629,1630,1633],{},[1590,1631,1632],{},"merge, combine, opération binaire",[1590,1634,1635],{},"Propriétés algébriques",[1637,1638,1639],"blockquote",{},[18,1640,1641,1644],{},[40,1642,1643],{},"Mon avis"," : ces six patterns sont universels, peu importe la library de property based testing ou le framework. La capacité à regarder une fonction et voir \"tiens, ça c'est un round-trip\" reste la même, en Kotlin comme avec un autre langage. C'est ça qu'il faut maitriser, pas la syntaxe.",[194,1646],{},[48,1648,1650],{"id":1649},"les-générateurs-le-property-based-testing-ne-vaut-que-ce-que-valent-ses-données","Les générateurs : le property based testing ne vaut que ce que valent ses données",[18,1652,1653],{},"Une propriété parfaite sur un générateur médiocre ne teste rien. C'est la moitié du boulot, souvent la plus délicate.",[18,1655,1656,1657,152,1659,152,1661,152,1664,1667,1668,152,1671,152,1674,1677,1678,1681],{},"Kotest fournit les briques de base : ",[22,1658,133],{},[22,1660,137],{},[22,1662,1663],{},"Arb.localDate()",[22,1665,1666],{},"Arb.list(gen, range)",". On les compose avec ",[22,1669,1670],{},".map",[22,1672,1673],{},".filter",[22,1675,1676],{},".orNull"," et surtout ",[22,1679,1680],{},"Arb.bind",".",[18,1683,1684,1685,1687],{},"Pour un objet simple, ",[22,1686,1680],{}," suffit et se lit tout seul :",[56,1689,1691],{"className":58,"code":1690,"language":60,"meta":61,"style":61},"val coordinatesArb: Arb\u003CCoordinates> = Arb.bind(\n    Arb.double(-90.0, 90.0),\n    Arb.double(-180.0, 180.0),\n) { lat, lng -> Coordinates(lat, lng) }\n",[22,1692,1693,1719,1742,1761],{"__ignoreMap":61},[65,1694,1695,1697,1700,1702,1704,1706,1709,1711,1714,1717],{"class":67,"line":68},[65,1696,772],{"class":90},[65,1698,1699],{"class":75}," coordinatesArb: ",[65,1701,124],{"class":449},[65,1703,446],{"class":75},[65,1705,716],{"class":449},[65,1707,1708],{"class":75},"> ",[65,1710,298],{"class":90},[65,1712,1713],{"class":75}," Arb.",[65,1715,1716],{"class":71},"bind",[65,1718,652],{"class":75},[65,1720,1721,1724,1727,1729,1732,1735,1737,1739],{"class":67,"line":94},[65,1722,1723],{"class":75},"    Arb.",[65,1725,1726],{"class":71},"double",[65,1728,399],{"class":75},[65,1730,1731],{"class":90},"-",[65,1733,1734],{"class":336},"90.0",[65,1736,152],{"class":75},[65,1738,1734],{"class":336},[65,1740,1741],{"class":75},"),\n",[65,1743,1744,1746,1748,1750,1752,1755,1757,1759],{"class":67,"line":111},[65,1745,1723],{"class":75},[65,1747,1726],{"class":71},[65,1749,399],{"class":75},[65,1751,1731],{"class":90},[65,1753,1754],{"class":336},"180.0",[65,1756,152],{"class":75},[65,1758,1754],{"class":336},[65,1760,1741],{"class":75},[65,1762,1763,1766,1768,1771],{"class":67,"line":247},[65,1764,1765],{"class":75},") { lat, lng ",[65,1767,318],{"class":90},[65,1769,1770],{"class":71}," Coordinates",[65,1772,1773],{"class":75},"(lat, lng) }\n",[18,1775,1776,1777,1780,1781,1783,1784,1786],{},"Là où ça se corse, c'est quand les champs sont ",[40,1778,1779],{},"corrélés",". Reprenons le ",[22,1782,778],{}," qui contient deux pièges cachés. Les coordonnées n'ont de sens que si latitude et longitude existent ensemble. Et la date de fin doit toujours rester après la date de début. Si on génère deux ",[22,1785,692],{}," indépendantes, la moitié seront dans le désordre.",[18,1788,1789],{},"La tentation, c'est de générer large puis de filtrer :",[56,1791,1793],{"className":58,"code":1792,"language":60,"meta":61,"style":61},"\u002F\u002F À éviter : jette la moitié des cas générés\nval datesArb = Arb.bind(startArb, endArb) { s, e -> s to e }\n    .filter { (s, e) -> s \u003C= e }\n",[22,1794,1795,1800,1821],{"__ignoreMap":61},[65,1796,1797],{"class":67,"line":68},[65,1798,1799],{"class":219},"\u002F\u002F À éviter : jette la moitié des cas générés\n",[65,1801,1802,1804,1807,1809,1811,1813,1816,1818],{"class":67,"line":94},[65,1803,772],{"class":90},[65,1805,1806],{"class":75}," datesArb ",[65,1808,298],{"class":90},[65,1810,1713],{"class":75},[65,1812,1716],{"class":71},[65,1814,1815],{"class":75},"(startArb, endArb) { s, e ",[65,1817,318],{"class":90},[65,1819,1820],{"class":75}," s to e }\n",[65,1822,1823,1826,1829,1832,1834,1837,1839],{"class":67,"line":111},[65,1824,1825],{"class":75},"    .",[65,1827,1828],{"class":71},"filter",[65,1830,1831],{"class":75}," { (s, e) ",[65,1833,318],{"class":90},[65,1835,1836],{"class":75}," s ",[65,1838,324],{"class":90},[65,1840,1841],{"class":75}," e }\n",[18,1843,1844,1845,1847],{},"C'est une mauvaise idée. Un ",[22,1846,1673],{}," qui rejette trop, c'est des générations gâchées et, au bout du compte, un test qui échoue quand Kotest ne produit plus assez de cas valides. Voici une rêgle utile : ne jamais filtrer ce qu'on peut construire directement.",[18,1849,1850],{},"Ici, on génère une date de début et une durée positive. L'ordre devient vrai par construction, sans aucun rejet :",[56,1852,1854],{"className":58,"code":1853,"language":60,"meta":61,"style":61},"val orderedDatesArb: Arb\u003CPair\u003CLocalDate, LocalDate>> = Arb.bind(\n    Arb.localDate(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 12, 31)),\n    Arb.int(0..300),\n) { start, span -> start to start.plusDays(span.toLong()) }\n",[22,1855,1856,1889,1938,1956],{"__ignoreMap":61},[65,1857,1858,1860,1863,1865,1867,1870,1872,1874,1876,1878,1881,1883,1885,1887],{"class":67,"line":68},[65,1859,772],{"class":90},[65,1861,1862],{"class":75}," orderedDatesArb: ",[65,1864,124],{"class":449},[65,1866,446],{"class":75},[65,1868,1869],{"class":449},"Pair",[65,1871,446],{"class":75},[65,1873,692],{"class":449},[65,1875,152],{"class":75},[65,1877,692],{"class":449},[65,1879,1880],{"class":75},">> ",[65,1882,298],{"class":90},[65,1884,1713],{"class":75},[65,1886,1716],{"class":71},[65,1888,652],{"class":75},[65,1890,1891,1893,1896,1899,1902,1904,1907,1909,1912,1914,1916,1919,1921,1923,1925,1927,1930,1932,1935],{"class":67,"line":94},[65,1892,1723],{"class":75},[65,1894,1895],{"class":71},"localDate",[65,1897,1898],{"class":75},"(LocalDate.",[65,1900,1901],{"class":71},"of",[65,1903,399],{"class":75},[65,1905,1906],{"class":336},"2024",[65,1908,152],{"class":75},[65,1910,1911],{"class":336},"1",[65,1913,152],{"class":75},[65,1915,1911],{"class":336},[65,1917,1918],{"class":75},"), LocalDate.",[65,1920,1901],{"class":71},[65,1922,399],{"class":75},[65,1924,1906],{"class":336},[65,1926,152],{"class":75},[65,1928,1929],{"class":336},"12",[65,1931,152],{"class":75},[65,1933,1934],{"class":336},"31",[65,1936,1937],{"class":75},")),\n",[65,1939,1940,1942,1944,1946,1948,1951,1954],{"class":67,"line":111},[65,1941,1723],{"class":75},[65,1943,84],{"class":71},[65,1945,399],{"class":75},[65,1947,1129],{"class":336},[65,1949,1950],{"class":90},"..",[65,1952,1953],{"class":336},"300",[65,1955,1741],{"class":75},[65,1957,1958,1961,1963,1966,1968,1971,1974],{"class":67,"line":247},[65,1959,1960],{"class":75},") { start, span ",[65,1962,318],{"class":90},[65,1964,1965],{"class":75}," start to start.",[65,1967,908],{"class":71},[65,1969,1970],{"class":75},"(span.",[65,1972,1973],{"class":71},"toLong",[65,1975,1976],{"class":75},"()) }\n",[18,1978,1979,1980,1982],{},"Même logique pour les coordonnées optionnelles : on construit l'objet complet, puis on le rend nullable d'un bloc avec ",[22,1981,1676],{},", ce qui évite le cas \"latitude sans longitude\".",[56,1984,1986],{"className":58,"code":1985,"language":60,"meta":61,"style":61},"val locationArb: Arb\u003CCoordinates?> = coordinatesArb.orNull(nullProbability = 0.3)\n",[22,1987,1988],{"__ignoreMap":61},[65,1989,1990,1992,1995,1997,1999,2001,2004,2006,2009,2012,2015,2017,2020],{"class":67,"line":68},[65,1991,772],{"class":90},[65,1993,1994],{"class":75}," locationArb: ",[65,1996,124],{"class":449},[65,1998,446],{"class":75},[65,2000,716],{"class":449},[65,2002,2003],{"class":75},"?> ",[65,2005,298],{"class":90},[65,2007,2008],{"class":75}," coordinatesArb.",[65,2010,2011],{"class":71},"orNull",[65,2013,2014],{"class":75},"(nullProbability ",[65,2016,298],{"class":90},[65,2018,2019],{"class":336}," 0.3",[65,2021,749],{"class":75},[18,2023,2024,2025,2027,2028,2031,2032,2035,2036,2038],{},"Un dernier point, déjà croisé dans le rappel : les ",[22,2026,124],{}," ",[40,2029,2030],{},"injectent automatiquement les cas limites",". C'est ce qui les sépare d'un ",[22,2033,2034],{},"Random"," maison, qui ne tombera quasiment jamais sur ",[22,2037,151],{}," ou sur la chaîne vide tout seul. Or c'est précisément là où se cachent les bugs. On les couvre sans même y penser. La proportion de cas limites se règle par configuration si le défaut ne convient pas.",[194,2040],{},[48,2042,2044],{"id":2043},"shrinking-pourquoi-un-échec-de-property-based-testing-est-exploitable","Shrinking : pourquoi un échec de property based testing est exploitable",[18,2046,2047,2048,2050,2051,2054],{},"Voici ce qui sépare le property based testing d'un simple ",[22,2049,2034],{}," dans une boucle. Quand une propriété échoue, Kotest ne renvoie pas le monstre de 200 caractères qui a déclenché le bug. Il ",[40,2052,2053],{},"réduit"," le contre-exemple vers le plus petit cas qui échoue encore.",[18,2056,2057],{},"Prenons cette remise, avec un bug volontaire : aucune garde sur un pourcentage supérieur à 100.",[56,2059,2061],{"className":58,"code":2060,"language":60,"meta":61,"style":61},"fun applyDiscount(price: Int, percent: Int): Double =\n    price - price * percent \u002F 100.0\n\ntest(\"une remise ne rend jamais le prix négatif\") {\n    checkAll(Arb.int(0..1000), Arb.int(0..200)) { price, percent ->\n        applyDiscount(price, percent) shouldBeGreaterThanOrEqual 0.0\n    }\n}\n",[22,2062,2063,2090,2112,2116,2127,2163,2174,2178],{"__ignoreMap":61},[65,2064,2065,2068,2071,2074,2077,2080,2082,2084,2087],{"class":67,"line":68},[65,2066,2067],{"class":90},"fun",[65,2069,2070],{"class":71}," applyDiscount",[65,2072,2073],{"class":75},"(price: ",[65,2075,2076],{"class":449},"Int",[65,2078,2079],{"class":75},", percent: ",[65,2081,2076],{"class":449},[65,2083,1008],{"class":75},[65,2085,2086],{"class":449},"Double",[65,2088,2089],{"class":90}," =\n",[65,2091,2092,2095,2097,2100,2103,2106,2109],{"class":67,"line":94},[65,2093,2094],{"class":75},"    price ",[65,2096,1731],{"class":90},[65,2098,2099],{"class":75}," price ",[65,2101,2102],{"class":90},"*",[65,2104,2105],{"class":75}," percent ",[65,2107,2108],{"class":90},"\u002F",[65,2110,2111],{"class":336}," 100.0\n",[65,2113,2114],{"class":67,"line":111},[65,2115,756],{"emptyLinePlaceholder":755},[65,2117,2118,2120,2122,2125],{"class":67,"line":247},[65,2119,396],{"class":71},[65,2121,399],{"class":75},[65,2123,2124],{"class":402},"\"une remise ne rend jamais le prix négatif\"",[65,2126,406],{"class":75},[65,2128,2129,2131,2133,2135,2137,2139,2141,2144,2147,2149,2151,2153,2155,2158,2161],{"class":67,"line":358},[65,2130,411],{"class":71},[65,2132,76],{"class":75},[65,2134,84],{"class":71},[65,2136,399],{"class":75},[65,2138,1129],{"class":336},[65,2140,1950],{"class":90},[65,2142,2143],{"class":336},"1000",[65,2145,2146],{"class":75},"), Arb.",[65,2148,84],{"class":71},[65,2150,399],{"class":75},[65,2152,1129],{"class":336},[65,2154,1950],{"class":90},[65,2156,2157],{"class":336},"200",[65,2159,2160],{"class":75},")) { price, percent ",[65,2162,91],{"class":90},[65,2164,2165,2168,2171],{"class":67,"line":461},[65,2166,2167],{"class":71},"        applyDiscount",[65,2169,2170],{"class":75},"(price, percent) shouldBeGreaterThanOrEqual ",[65,2172,2173],{"class":336},"0.0\n",[65,2175,2176],{"class":67,"line":721},[65,2177,458],{"class":75},[65,2179,2180],{"class":67,"line":734},[65,2181,114],{"class":75},[18,2183,2184],{},"Le test échoue, mais regardons ce que Kotest renvoie :",[56,2186,2191],{"className":2187,"code":2188,"language":2189,"meta":2190,"style":61},"language-plain shiki shiki-themes github-dark-default","Property failed after 12 attempts\n\n    Arg 0: 1 (shrunk from 743)\n    Arg 1: 101 (shrunk from 187)\n\nRepeat this test by using seed 8842176331705615667\n","plain","text",[22,2192,2193,2198,2202,2207,2212,2216],{"__ignoreMap":61},[65,2194,2195],{"class":67,"line":68},[65,2196,2197],{},"Property failed after 12 attempts\n",[65,2199,2200],{"class":67,"line":94},[65,2201,756],{"emptyLinePlaceholder":755},[65,2203,2204],{"class":67,"line":111},[65,2205,2206],{},"    Arg 0: 1 (shrunk from 743)\n",[65,2208,2209],{"class":67,"line":247},[65,2210,2211],{},"    Arg 1: 101 (shrunk from 187)\n",[65,2213,2214],{"class":67,"line":358},[65,2215,756],{"emptyLinePlaceholder":755},[65,2217,2218],{"class":67,"line":461},[65,2219,2220],{},"Repeat this test by using seed 8842176331705615667\n",[18,2222,2223,2224,2227,2228,2231],{},"Le tir initial était ",[22,2225,2226],{},"743, 187"," ce qui est illisible. Le shrinking l'a ramené à ",[22,2229,2230],{},"1, 101"," : le plus petit prix et le plus petit pourcentage qui casse. Le bug saute alors aux yeux, la frontière est à 100. On débogue le cas minimal plutôt qu'un tirage au hasard.",[18,2233,2234,2235,2240,2241,2245],{},"Et cette ",[34,2236,2239],{"href":2237,"rel":2238},"https:\u002F\u002Fkotest.io\u002Fdocs\u002Fproptest\u002Fproperty-test-seeds.html",[38],"seed ","en bas n'est pas décorative. Elle rejoue exactement la même séquence de génération. Un échec de property based testing est donc parfaitement reproductible, contrairement à ce qu'on croit souvent. (Le shrinking de jqwik va plus loin sur les cas récursifs, mais j'en ai déjà parlé dans le ",[34,2242,2244],{"href":36,"rel":2243},[38],"REX",", je ne refais pas le match ici.)",[194,2247],{},[48,2249,2251],{"id":2250},"en-pratique-avec-kotest-la-mécanique-qui-compte","En pratique avec Kotest : la mécanique qui compte",[18,2253,2254],{},"Quelques réflexes qui font gagner du temps une fois qu'on écrit du property based testing.",[18,2256,2257,2027,2262,2027,2265,2027,2270,2272,2273,2275,2276,2279,2280,2282],{},[40,2258,2259],{},[22,2260,2261],{},"forAll",[40,2263,2264],{},"ou",[40,2266,2267,2269],{},[22,2268,72],{},"****.",[22,2271,2261],{}," attend un booléen, ",[22,2274,72],{}," attend des assertions (",[22,2277,2278],{},"shouldBe"," et compagnie). En pratique, ",[22,2281,72],{}," presque tout le temps : les assertions donnent de meilleurs messages d'erreur et on peut en enchaîner plusieurs.",[56,2284,2286],{"className":58,"code":2285,"language":60,"meta":61,"style":61},"\u002F\u002F booléen, concis\nforAll(Arb.int()) { it + 0 == it }\n\n\u002F\u002F assertions, plus lisible en cas d'échec\ncheckAll(Arb.int()) { n -> (n + 0) shouldBe n }\n",[22,2287,2288,2293,2316,2320,2325],{"__ignoreMap":61},[65,2289,2290],{"class":67,"line":68},[65,2291,2292],{"class":219},"\u002F\u002F booléen, concis\n",[65,2294,2295,2297,2299,2301,2304,2307,2310,2313],{"class":67,"line":94},[65,2296,2261],{"class":71},[65,2298,76],{"class":75},[65,2300,84],{"class":71},[65,2302,2303],{"class":75},"()) { it ",[65,2305,2306],{"class":90},"+",[65,2308,2309],{"class":336}," 0",[65,2311,2312],{"class":90}," ==",[65,2314,2315],{"class":75}," it }\n",[65,2317,2318],{"class":67,"line":111},[65,2319,756],{"emptyLinePlaceholder":755},[65,2321,2322],{"class":67,"line":247},[65,2323,2324],{"class":219},"\u002F\u002F assertions, plus lisible en cas d'échec\n",[65,2326,2327,2329,2331,2333,2336,2338,2341,2343,2345],{"class":67,"line":358},[65,2328,72],{"class":71},[65,2330,76],{"class":75},[65,2332,84],{"class":71},[65,2334,2335],{"class":75},"()) { n ",[65,2337,318],{"class":90},[65,2339,2340],{"class":75}," (n ",[65,2342,2306],{"class":90},[65,2344,2309],{"class":336},[65,2346,2347],{"class":75},") shouldBe n }\n",[18,2349,2350,2353,2354,2357],{},[40,2351,2352],{},"Régler le nombre d'itérations et la seed"," via ",[22,2355,2356],{},"PropTestConfig",". Utile pour pousser une propriété critique ou figer une seed le temps d'investiguer.",[56,2359,2361],{"className":58,"code":2360,"language":60,"meta":61,"style":61},"checkAll(PropTestConfig(iterations = 10_000), myArb) { \u002F* ... *\u002F }\n",[22,2362,2363],{"__ignoreMap":61},[65,2364,2365,2367,2369,2371,2374,2376,2379,2382,2385],{"class":67,"line":68},[65,2366,72],{"class":71},[65,2368,399],{"class":75},[65,2370,2356],{"class":71},[65,2372,2373],{"class":75},"(iterations ",[65,2375,298],{"class":90},[65,2377,2378],{"class":336}," 10_000",[65,2380,2381],{"class":75},"), myArb) { ",[65,2383,2384],{"class":219},"\u002F* ... *\u002F",[65,2386,1132],{"class":75},[18,2388,2389,2027,2393,2027,2395,2027,2399,2401,2402,2404,2405,2408],{},[40,2390,2391],{},[22,2392,124],{},[40,2394,2264],{},[40,2396,2397,2269],{},[22,2398,166],{},[22,2400,124],{}," échantillonne un grand espace au hasard. ",[22,2403,166],{}," teste ",[40,2406,2407],{},"tous"," les cas d'un petit domaine fini, un enum ou un booléen par exemple. Pour un enum de stratégies, l'exhaustif garantit qu'on passe sur chacune.",[56,2410,2412],{"className":58,"code":2411,"language":60,"meta":61,"style":61},"checkAll(Exhaustive.enum\u003CRelaxStrategy>()) { strategy -> \u002F* ... *\u002F }\n",[22,2413,2414],{"__ignoreMap":61},[65,2415,2416,2418,2421,2423,2425,2427,2430,2432,2435],{"class":67,"line":68},[65,2417,72],{"class":71},[65,2419,2420],{"class":75},"(Exhaustive.",[65,2422,762],{"class":71},[65,2424,446],{"class":75},[65,2426,1063],{"class":449},[65,2428,2429],{"class":75},">()) { strategy ",[65,2431,318],{"class":90},[65,2433,2434],{"class":219}," \u002F* ... *\u002F",[65,2436,1132],{"class":75},[18,2438,2439,2442,2443,2448],{},[40,2440,2441],{},"Attention au coût en contexte Spring."," Une propriété qui démarre un contexte, tape sur une base ou un container (via ",[34,2444,2447],{"href":2445,"rel":2446},"https:\u002F\u002Ftestcontainers.com\u002F",[38],"Testcontainers"," par exemple) pour chaque itération, c'est des milliers d'appels, des containers qui démarrent. Mieux vaut garder le property based testing sur le domaine pur, là où les fonctions ne démarrent pas l'appli. Pour l'intégration, l'exemple ciblé reste plus raisonnable.",[194,2450],{},[48,2452,2454],{"id":2453},"le-coût-honnête-et-quand-sen-passer","Le coût honnête et quand s'en passer",[18,2456,2457],{},"Le property based testing n'est pas gratuit. Prétendre le contraire rendrait cet article malhonnête.",[18,2459,2460,2461,2464,2465,2468,2469,2472],{},"C'est ",[40,2462,2463],{},"plus dur à écrire que des TU simples",". Trouver la propriété demande de bien comprendre le domaine. C'est ",[40,2466,2467],{},"plus lent"," à l'exécution, potentiellement des centaines d'itérations par test. Et surtout, un ",[40,2470,2471],{},"générateur incorrect donne une fausse confiance"," : s'il ne produit jamais le cas qui fait mal, la propriété reste verte pour de mauvaises raisons.",[18,2474,2475,2476,2479],{},"Un piège assez vicieux est ailleurs. Par exemple, un oracle de test buggé qui fait passer une propriété à vide. J'ai vu du code de test qui comparait deux objets par réflexion avec un ",[22,2477,2478],{},"try\u002Fcatch"," avalant silencieusement les erreurs d'accès. Résultat : les champs illisibles passaient pour identiques. La propriété validait alors des transformations qu'elle ne regardait même pas. C'est le mutant survivant de mon REX, mais appliqué au test lui-même. Un test qui ne peut pas échouer ne teste rien, property based testing ou pas.",[18,2481,2482,2483,2486,2487,2489,2490,2492],{},"Dernier point, très important : ",[40,2484,2485],{},"le property based testing complète les tests par l'exemple, il ne s'y substitue pas."," La propriété du ",[22,2488,778],{}," prouve les invariants sur tout l'espace d'entrée. Elle ne dira jamais que ",[22,2491,1250],{}," multiplie par 1,25 exactement. Pour ça, il faut l'exemple, précis, qui documente la règle métier et verrouille la régression connue. On combine :",[2494,2495,2496,2504],"ul",{},[2497,2498,2499,2500,2503],"li",{},"des ",[40,2501,2502],{},"exemples"," pour les valeurs exactes et les cas historiques qui ont déjà échoués,",[2497,2505,2499,2506,2509],{},[40,2507,2508],{},"propriétés"," là où le domaine porte une vérité générale.",[18,2511,2512,2513,2516,2517,2522,2523,2527],{},"Et la boucle se referme sur le reste de la série. Le property based testing rend un test plus dur à tromper dès l'écriture, ce que la discipline du ",[127,2514,2515],{},"red"," en ",[34,2518,2521],{"href":2519,"rel":2520},"https:\u002F\u002Fblog.hoppr.tech\u002Fblogs\u002F2026-06-30-tdd-jen-entends-beaucoup-parler-mais-cest-quoi-au-juste",[38],"TDD"," fait déjà à la main. Le ",[34,2524,2526],{"href":36,"rel":2525},[38],"mutation testing"," juge, lui, la qualité des tests après coup. Ce sont trois angles pour la même obsession : arrêter de faire confiance au vert sur parole.",[194,2529],{},[48,2531,2533],{"id":2532},"le-pattern-dabord-la-lib-ensuite","Le pattern d'abord, la lib ensuite",[18,2535,2536],{},"Le property based testing outille le jugement. On affirme une vérité générale et la machine cherche à la contredire sur des cas auxquels on n'aurait jamais pensé. Quand elle y arrive, le shrinking réduit l'échec au plus petit cas qui casse.",[18,2538,2539],{},"Ce qu'on retient de cet article tient dans ces six questions, pas dans une syntaxe. Devant une fonction, on se demande : y a-t-il un aller-retour, une idempotence, un invariant, une sortie facile à vérifier, un oracle, une propriété algébrique ? Elles valent en Kotest, en jqwik et dans la lib qui sortira dans cinq ans.",[18,2541,2542],{},"Par où commencer dans une vraie codebase ? Je vise d'abord deux endroits : les frontières de sérialisation et les refactos à iso-comportement. Ce sont ceux où une propriété rapporte le plus, pour l'effort le plus faible.",[194,2544],{},[48,2546,2548],{"id":2547},"pour-aller-plus-loin","Pour aller plus loin",[18,2550,2551],{},[40,2552,2553],{},"La série sur le blog",[2494,2555,2556,2562,2573],{},[2497,2557,2558,2561],{},[34,2559,42],{"href":36,"rel":2560},[38],", le point de départ de cet article",[2497,2563,2564,2568,2569,2572],{},[34,2565,2567],{"href":2519,"rel":2566},[38],"TDD : c'est quoi au juste ?",", la discipline du ",[127,2570,2571],{},"red\u002Fgreen"," qui inspire le property based testing",[2497,2574,2575,2579],{},[34,2576,2578],{"href":1363,"rel":2577},[38],"Du prototype à la prod : une solution IA solide",", pour le fil \"vérifier le code généré\"",[18,2581,2582],{},[40,2583,2584],{},"Property based testing",[2494,2586,2587,2595,2603],{},[2497,2588,2589,2594],{},[34,2590,2593],{"href":2591,"rel":2592},"https:\u002F\u002Fkotest.io\u002Fdocs\u002Fproptest\u002Fproperty-based-testing.html",[38],"Kotest, property-based testing",", la doc de référence",[2497,2596,2597,2602],{},[34,2598,2601],{"href":2599,"rel":2600},"https:\u002F\u002Ffsharpforfunandprofit.com\u002Fposts\u002Fproperty-based-testing-2\u002F",[38],"Choosing properties for property-based testing",", la source de ces patterns, par Scott Wlaschin. Il en liste sept, le septième porte sur les structures récursives",[2497,2604,2605,2610],{},[34,2606,2609],{"href":2607,"rel":2608},"https:\u002F\u002Fjqwik.net\u002F",[38],"jqwik"," côté JUnit 5, comparé à Kotest dans mon REX",[2612,2613,2614],"style",{},"html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}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 .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 .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}",{"title":61,"searchDepth":94,"depth":94,"links":2616},[2617,2618,2619,2628,2629,2630,2631,2632,2633],{"id":50,"depth":94,"text":51},{"id":198,"depth":94,"text":199},{"id":368,"depth":94,"text":369,"children":2620},[2621,2622,2623,2624,2625,2626,2627],{"id":376,"depth":111,"text":377},{"id":556,"depth":111,"text":557},{"id":622,"depth":111,"text":623},{"id":1254,"depth":111,"text":1255},{"id":1373,"depth":111,"text":1374},{"id":1423,"depth":111,"text":1424},{"id":1566,"depth":111,"text":1567},{"id":1649,"depth":94,"text":1650},{"id":2043,"depth":94,"text":2044},{"id":2250,"depth":94,"text":2251},{"id":2453,"depth":94,"text":2454},{"id":2532,"depth":94,"text":2533},{"id":2547,"depth":94,"text":2548},"2026-07-28T09:48:44.449Z","Un test unitaire, tout le monde connait : on choisit une entrée, on vérifie la sortie. `[3, 1, 2]` trié doit rendre `[1, 2, 3]`. C'est efficace, mais on ne teste que les cas qu'on a imaginés. Le prope","md",".\u002Fassets\u002Fcover-image.webp",{},"\u002Fblogs\u002F2026-07-28-property-based-testing-comment-bien-choisir-ses-proprietes-avec-kotest",[2641],{"id":2642,"name":2643,"image":2644,"linkedin":2645,"x":13,"jobTitle":2646},"e8163b24-7e01-41c5-adbf-0dc655f929d0","Nicolas Zago",".\u002Fassets\u002Freviewer-nicolas-zago.webp","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fnicolaszago\u002F","CEO HoppR",{"title":5,"description":2635},"blogs\u002F2026-07-28-property-based-testing-comment-bien-choisir-ses-proprietes-avec-kotest\u002Findex",[2650,60,396,2651],"craft","testing","tlL-TyD7hYohxS9h1Y_5c5al9Ktt9sC6IhKIZ-qqB_U",[2654,2664,2671,2681],{"path":2655,"title":42,"image":2656,"date":2657,"tags":2658},"\u002Fblogs\u002F2026-06-23-le-vert-ne-suffit-pas-3-facons-de-douter-de-son-code","\u002Fcontent-assets\u002F2026-06-23-le-vert-ne-suffit-pas-3-facons-de-douter-de-son-code\u002Fassets\u002Fcover-image.webp","23 juin 2026",[2650,396,2651,2659,2660,60,2661,2662,2663],"java","rex","événement","2026","veille tech",{"path":2665,"title":2666,"image":2667,"date":2668,"tags":2669},"\u002Fblogs\u002F2026-06-30-tdd-jen-entends-beaucoup-parler-mais-cest-quoi-au-juste","TDD : J’en entends beaucoup parler, mais c’est quoi au juste ?","\u002Fcontent-assets\u002F2026-06-30-tdd-jen-entends-beaucoup-parler-mais-cest-quoi-au-juste\u002Fassets\u002Fcover-image.webp","30 juin 2026",[2651,2650,2670,2659,396],"tdd",{"path":2672,"title":2673,"image":2674,"date":2675,"tags":2676},"\u002Fblogs\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide","Du prototype à la prod : ce qu'on ne te dit pas sur la construction d'une solution IA solide","\u002Fcontent-assets\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide\u002Fassets\u002Fcover-image.webp","9 juin 2026",[2677,396,2651,2678,2679,2659,2680,2650],"ia","observabilité","typescript","architecture",{"path":2682,"title":2683,"image":2684,"date":2685,"tags":2686},"\u002Fblogs\u002F2025-08-01-refactorer-sans-casser-le-golden-master-appliqu-un-outil-finops","Refactorer sans casser : le Golden Master appliqué à un outil FinOPS","\u002Fcontent-assets\u002F2025-08-01-refactorer-sans-casser-le-golden-master-appliqu-un-outil-finops\u002Fassets\u002Fcover-image.webp","1 août 2025",[2650,2663,396,2651],1785232218709]