Tester le hasard : un cas d’école du Property-Based Testing
Le PBT teste qu'un invariant métier est toujours vérifié quels que soient les paramètres et la valeur de retour de la fonction objet du test.
Publié le 10 décembre 2025

Le test déterministe, basé sur des exemples tels que
f(0) = 1 n'est qu'une modalité de test parmi d'autres . Dans certains cas, tels celui de fonctions reposant sur la génération de nombres au hasard, on peut faire mieux.Formulation du problème
Imaginons une fonction
random renvoyant un nombre réel compris entre deux bornes min et max, dont l'implémentation s'appuierait naturellement sur la fonction Math.random, qui renvoie un nombre réel compris entre 0 et 1 :type Parameters = {
min: number;
max: number;
};
const random = ({ min, max }: Parameters): number =>
min + (max - min) * Math.random();Comment tester cette fonction ?
Cette question demande en fait de savoir ce que l'on veut tester :
- Le fait que les valeurs générées sont bien comprises entre
etmin
?max - Ou bien la distribution de probabilités entre
etmin
?max
Dans notre cas, nous pouvons faire l'hypothèse que la distribution des valeurs de
Math.random est effectivement uniforme, conformément à sa spécification . C'est donc la valeur ajoutée de notre fonction, à savoir le fait "d'étirer" le hasard entre min et max, que nous souhaitons éprouver.Comment procéder ? Le (pseudo)-hasard ayant le bon goût d'être imprévisible, nous ne saurions procéder à un test déterministe, basé sur un exemple. En effet, tester que
random({ min: 1, max: 10 }) === 5 serait non seulement faux la plupart du temps, mais surtout cela n'aurait aucun sens.Approche 1 : l'inversion de dépendance pour "contrôler le hasard"
Une première solution est d'injecter la fonction génératrice de hasard afin de pouvoir contrôler sa valeur de retour dans un test. Pour cela, nous procédons par inversion de dépendance (IoD) : initialement, la fonction
random dépendait de Math.random ; à présent, elle définit elle-même et avec ses propres mots ce dont elle a besoin pour travailler, c'est-à-dire RandomGenerator, et charge au "monde extérieur", c'est-à-dire à l'appelant, d'injecter une fonction qui satisfait ce contrat (injection de dépendance).Ce qui nous donne le code suivant :
type RandomGenerator = () => number;
type Parameters = {
min: number;
max: number;
};
export const random =
(generator: RandomGenerator) =>
({ min, max }: Parameters): number =>
min + (max - min) * generator();
test("The lowest value possible should be 'min'", () => {
const generator = () => 0;
expect(random(generator)({ min: 1, max: 10 })).toEqual(1);
});
test("The highest value possible should be 'max'", () => {
const generator = () => 1;
expect(random(generator)({ min: 1, max: 10 })).toEqual(10);
});Cette solution fonctionne, mais vous conviendrez avec moi qu'elle est tout de même très aware de l'implémentation. Dit de façon moins "Jean-Claude ", cela signifie que si l'implémentation change, le test doit changer lui aussi. Nous souhaitons tester un certain comportement, peu importe l'implémentation, point.
Heureusement, on peut faire mieux.
Approche 2 : le Property-based testing pour tester un invariant métier
Par contraste avec l'Example-based testing (le test déterministe tel que nous le connaissons), le Property-based testing se propose de vérifier qu'un invariant métier est toujours vérifié pour des valeurs d'entrée prises au hasard et en grand nombre (généralement > 100, mais vous pouvez paramétrer le nombre de tirages).
Voyons ce que cela donne avec la bibliothèque
fast-check :import fc from "fast-check";
test("The generated value should fall within the (min, max) interval", () => {
fc.assert(
fc.property(
fc.record({ x1: fc.integer(), x2: fc.integer({ min: 1 }) }),
({ x1, x2 }): boolean => {
const min = x1;
const max = min + x2;
const actual = random({ min, max });
return min <= actual && actual <= max;
}
)
);
});Ici,
fast-check tire pour nous deux nombres entiers relatifs au hasard, x1 et x2, dont nous nous servons pour construire l'intervalle [min, max]. Ensuite, nous invoquons la fonction random objet de notre test et vérifions simplement que la valeur qu'elle retourne est bien comprise entre les bornes de l'intervalle. Remarquez qu'à aucun moment nous n'avons connaissance des valeurs de min, max, ni de random({ min, max }). C'est toute la beauté de la chose : nous testons un invariant.Ce que garantit vraiment le Property-based testing
Cet exemple est l'un des plus simples que je puisse envisager pour expliquer le Property-based testing, mais il est typique, car le hasard est un cas d'usage parfait de ce type de test.
Quelle sécurité ce type d'assertion nous procure-t-il ?
Ce test ne prouve rien de plus qu'un test classique : ce n'est pas une démonstration du bon fonctionnement de votre code, simplement un test. Même si vous tirez 1000 valeurs à chaque test, que vous effectuez le test 100 fois et que la propriété est vérifiée les 100000 fois, vous ne pouvez pas exclure l'éventualité de tomber sur un contre-exemple la 100001 ème fois. Mais statistiquement, c'est tout de même assez peu probable, et pour du développement, nous pouvons nous en satisfaire.
Gardez simplement à l'esprit que vous augmentez votre degré de sécurité en augmentant le nombre de tirages.