veilletech.fr
9 sept. Feed du jour
#06 PHP Article

Pest génère les entrées de test qu'on oublie

Un test vert veut dire « rien trouvé en deux mille essais ». Pas « pas de bug ».

Le paquet Fuzz de Jon Purvis apporte le fuzzing guidé par la couverture dans Pest 5, en s'appuyant sur PHP-Fuzzer de nikic : il déforme des chaînes de départ, observe les chemins parcourus dans le code et conserve celles qui atteignent une branche neuve. Sur l'exemple d'un parseur de limite de débit, il trouve l'entrée « 5/ » qui provoque une DivisionByZeroError. Aucune extension de couverture n'est nécessaire, mais la fonction cible doit être définie hors du test, sans quoi rien n'est instrumenté.

3 min de lectureintermédiairevidéo 1:21
Partager
Sommaire5 sections
  1. Ce qui se passe
  2. Ce que « guidé par la couverture » veut dire
  3. À quoi ça ressemble
  4. Deux pièges
  5. À retenir

Ce qui se passe

Fuzz, un paquet de Jon Purvis, permet d'écrire un test de fuzzing à l'intérieur d'un test Pest 5. Il s'appuie sur PHP-Fuzzer de nikic pour déformer les chaînes que vous fournissez et remonter les échecs par Pest.

Terminal
composer require jonpurvis/fuzz --dev

Prérequis : PHP 8.4+ et Pest 5.

Ce que « guidé par la couverture » veut dire

Un fuzzer ordinaire ajoute, retire ou remplace des morceaux d'une chaîne, puis exécute votre code avec le résultat. Un fuzzer guidé par la couverture observe en plus quels chemins ces entrées empruntent. Quand une entrée atteint une route jamais explorée, il la conserve et s'en sert pour fabriquer d'autres variantes ; la collection ainsi constituée s'appelle le corpus.

L'intérêt se voit sur un parseur : si le code vérifie d'abord la présence d'un crochet ouvrant, la plupart des entrées aléatoires échouent sur ce premier test. Dès qu'une entrée le franchit, le fuzzer peut continuer à la déformer pour atteindre le code qui suit.

PHP-Fuzzer collecte ce retour en suivant les transitions entre blocs de code et leur fréquence approximative. Fuzz s'en charge pour vous : ni Xdebug ni l'option --coverage de Pest ne sont nécessaires.

À quoi ça ressemble

L'exemple de l'article part d'un utilitaire qui lit une limite de débit du type 100/60s et la convertit en requêtes par seconde, sans valider son entrée :

PHP
use App\RateLimit;
use function Fuzz\fuzz;

$target = static function (string $input): void {
    RateLimit::perSecond($input);
};

test('rate limit spec parser never fatals', function () use ($target): void {
    fuzz($target)
        ->seed(['100/60s', '5/1s', '1000/3600s'])
        ->withDictionary(['/', 's', '0', '1'])
        ->runs(2000)
        ->maxLen(16)
        ->run('rate-limit-parser');
});

seed() donne les exemples de départ, withDictionary() fournit des fragments que le fuzzer peut insérer sans s'y limiter, runs() fixe le budget de recherche et maxLen() la taille des chaînes générées. Le nom passé à run() sert à séparer corpus et fichiers de plantage entre les tests.

Dans l'exécution décrite, Fuzz trouve 5/ : la fenêtre manquante devient une chaîne vide, PHP la convertit en 0, et la division lève une DivisionByZeroError. L'entrée fautive est écrite dans .pest/fuzz-crashes/, donc rejouable telle quelle.

Deux pièges

La fonction cible se définit en dehors de test(). Fuzz l'exécute dans un processus PHP séparé, où la classe de test générée par Pest n'existe pas. Sur la version 1.0.1, la vérification de l'article montre que le passage direct d'un Closure::fromCallable() n'enregistre aucune couverture, là où le wrapper externe fonctionne.

Un test qui passe ne prouve rien d'autre que l'absence de trouvaille. Fuzz signale les TypeError ainsi que les warnings et notices non supprimés, mais ignore les exceptions ordinaires par défaut — y compris les exceptions de validation Laravel. La méthode allow() permet de restreindre cette liste quand une exception inattendue doit faire échouer le test. Un timeout() par entrée existe aussi, mais réclame l'extension pcntl.

Autre limite du cas d'exemple : ce test ne détecte qu'un plantage, pas une valeur de retour fausse. Pour couvrir cela, il faut placer une attente Pest dans la fonction cible — par exemple vérifier qu'encoder puis décoder rend la chaîne d'origine.

À retenir

Le fuzzing ne remplace pas vos jeux de données : gardez-les pour les cas connus et les résultats attendus. Fuzz est utile là où le code accepte plus d'entrées que vous ne pouvez en lister — un parseur, un format de saisie utilisateur. Et la guidance par couverture rend le plus de service sur les codes à validations successives, où franchir un contrôle donne accès au suivant.

Pratique : un petit budget dans la suite habituelle, une recherche longue dans un job de CI planifié.

Source : Find Unexpected Test Inputs with Fuzz for Pest