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
    min
    et
    max
     ?
  • Ou bien la distribution de probabilités entre
    min
    et
    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.