Introduction : pourquoi les tests unitaires ne suffisent pas à tout
\n\nImaginez que vous ayez écrit une fonction de tri de tableau. Vous l'avez couverte d'une dizaine de tests unitaires : tableau vide, un seul élément, liste déjà triée, ordre inverse. Les tests sont verts. Vous avez confiance en votre code. Mais une semaine plus tard, la production plante — sur un tableau de 1000 éléments avec des doublons et des nombres négatifs. Cela vous dit quelque chose ?
\n\nLes tests TypeScript traditionnels basés sur des exemples (example-based testing) ne vérifient que les scénarios que vous avez imaginés. Et si un bug se cachait dans une combinaison à laquelle vous n'avez pas pensé ? C'est là qu'intervient le test basé sur les propriétés (property-based testing) — une approche qui génère automatiquement des milliers de données d'entrée aléatoires et vérifie les invariants de votre code.
\n\nDans cet article, nous verrons comment appliquer le property-based testing en TypeScript avec la bibliothèque fast-check, quelles propriétés vérifier et comment cela change l'approche du développement.
\n\nQu'est-ce que le test basé sur les propriétés ?
\n\nAu lieu d'écrire :
\nit('trie [3, 1, 2] → [1, 2, 3]', () => {\n expect(sort([3, 1, 2])).toEqual([1, 2, 3]);\n});\n\n\nVous décrivez une propriété (property) — une assertion qui doit être vraie pour toutes les données d'entrée valides :
\nit('un tableau trié ne modifie pas l'ordre des éléments', () => {\n fc.assert(\n fc.property(fc.array(fc.integer()), (arr) => {\n const sorted = sort(arr);\n // Propriété 1 : la longueur est conservée\n expect(sorted.length).toBe(arr.length);\n // Propriété 2 : chaque élément du tableau d'origine est présent\n // Propriété 3 : le tableau est trié par ordre croissant\n for (let i = 1; i < sorted.length; i++) {\n expect(sorted[i - 1]).toBeLessThanOrEqual(sorted[i]);\n }\n })\n );\n});\n\n\nLa bibliothèque fast-check générera elle-même des centaines de tableaux aléatoires (y compris vides, avec NaN, avec des nombres décimaux, avec des valeurs énormes) et exécutera votre test sur chacun d'eux. Si l'un d'eux échoue — vous serez informé du problème.
\n\nPourquoi est-ce particulièrement utile en TypeScript ?
\n\nTypeScript offre un typage statique, mais il ne garantit pas la correction de la logique. Le test basé sur les propriétés en TypeScript comble le fossé entre « les types correspondent » et « le code fonctionne correctement ». C'est particulièrement utile pour :
\n\n- \n
- Les fonctions de transformation de données (map, filter, reduce) \n
- Les analyseurs et validateurs \n
- Les algorithmes à logique non triviale \n
- Les fonctions manipulant des valeurs limites \n
La bibliothèque fast-check : démarrage rapide
\n\nInstallez fast-check dans votre projet :
\nnpm install --save-dev fast-check\n\n\nSyntaxe de base :
\nimport fc from 'fast-check';\n\ndescribe('propriétés de la fonction reverse', () => {\n it('reverse(reverse(arr)) === arr', () => {\n fc.assert(\n fc.property(fc.array(fc.anything()), (arr) => {\n expect(reverse(reverse(arr))).toEqual(arr);\n })\n );\n });\n});\n\n\nIci, fc.array(fc.anything()) est un générateur de tableaux arbitraires. Vous pouvez combiner les générateurs :
- \n
fc.integer()— nombres entiers \n fc.string()— chaînes de caractères \n fc.record({ name: fc.string(), age: fc.integer({ min: 0, max: 120 }) })— objets \n fc.oneof(...)— sélection aléatoire parmi plusieurs générateurs \n
Quelles propriétés vérifier ? 5 exemples pratiques
\n\n1. Invariance (réversibilité)
\nToute opération qui a une opération inverse doit être réversible :
\nit('sérialisation et désérialisation JSON', () => {\n fc.assert(\n fc.property(fc.object(), (obj) => {\n const json = JSON.stringify(obj);\n const parsed = JSON.parse(json);\n expect(parsed).toEqual(obj);\n })\n );\n});\n<