3 techniques pour gérer l'aléatoire en Approval Testing
This article is available in English.
Lorsqu'on est confronté à du code legacy, se retrouver face à de l'aléatoire n'est pas ce qu'il y a de plus sympa.
Mettre en place des tests pour couvrir un système qui produit des résulats aléatoires peut s'avérer assez vite difficile.
Comme tout le monde, vous voulez que vos tests soient déterministes et vous donnent toujours le même résultat, quand vous n'avez rien changé dans le système.
Voici trois idées qui peuvent vous aider à gérer l'aléatoire dans le système. Les deux premières techniques sont fondées sur l'idée de supprimer la partie aléatoire du système pendant les tests.
Bloquer le générateur d'aléatoire
La première solution que je souhaite partager dans cet article consiste à garantir que le générateur aléatoire renvoie toujours les mêmes résultats lorsqu'il est appelé. La réponse ici est de configurer le générateur avec une valeur que vous contrôlez, afin de rendre le générateur déterministe.
Par exemple, en PHP, vous pouvez appeler la fonction mt_rand
. Plus tard, tout appel à la fonction rand
renverra les mêmes nombres aléatoires dans le même ordre.
C'est une manière rapide de transformer un système non déterministe en quelque chose qui donne toujours les mêmes résultats.
Malheureusement, cette solution, tout en étant l'une des façons les plus rapides de résoudre le problème, ne fonctionne que dans certaines situations. Tout d'abord, toutes les langages ne permettent pas de configurer le générateur de nombres aléatoires. Si ce n'est pas possible il vous faute une autre solution.
Un autre inconvénient de cette solution est que si vous modifiez la structure de votre code et que l'ordre des appels à une méthode aléatoire est changé vous obtiendrez toujours les mêmes valeurs dans le même ordre mais pas dans le même espace logique. Par exemple, prenons deux appels à rand
et assignons le résultat du premier appel à la variable A et du second appel à la variable B. Après avoir configuré le générateur de nombres aléatoires, A et B ont toujours les mêmes valeurs, disons 34 et 1879. Lors d'un refactoring, si nous décidons d'inverser l'ordre d'assignation, nous avons un problème. Le code assigne maintenant la valeur de B avant celle de A ; A vaut maintenant 1879 et B vaut 39. Vous pouvez expérimenter cette idée ici.
Que pouvons-nous faire lorsque la configuration du générateur de nombres aléatoires est impossible, ou si nous souhaitons peut-être changer l'ordre des appels à un moment donné ?
Contrôler l'aléatoire
L'autre alternative est de reprendre le contrôle sur la génération des valeurs. Comme souvent, ajouter une couche d'abstraction est l'une des solutions disponibles dans notre boîte à outils. Ici, nous souhaitons briser la dépendance directe envers le générateur de valeurs aléatoires et le remplacer par une dépendance que nous pouvons manipuler.
En termes plus concrets, au lieu d'appeler directement une fonction du langage, nous l'encapsulons, créons un stub que nous pouvons manipuler, et trouvons un moyen de substituer l'implémentation originale avec notre doublure.
Les techniques de substitution disponibles varient grandement en fonction du langage de programmation utilisé et de l'état du code. Si vous avez la chance de pouvoir injecter des dépendances lors de la construction, c'est une tâche facile. Si ce n'est pas le cas, certains patterns issus de Working Effectively With Legacy Code par Michael Feathers peuvent s'avérer utiles.
Cette solution est disponible dans tous les langages. La seule exigence est de se sentir à l'aise avec la manipulation de code sans avoir de tests pour pouvoir reprendre le contrôle.
Une fois que vous avez le contrôle, vous pouvez décider des valeurs générées.
Cette solution nous permet de résoudre le problème d'ordre mentionné précédemment. Si vous inversez l'ordre des appels dans le code, vous pouvez changer la configuration de votre stub pour changer l'ordre et maintenir les tests valides.
Modifier le stub après la création des tests doit être fait avec prudence. Si vous voulez plus de confiance et éviter de toucher aux tests, vous avez une autre alternative. Au lieu de créer une abstraction pour chaque appel au générateur aléatoire, vous pouvez en créer une pour chaque appel. Reprenant l'exemple de la première partie, vous pouvez introduire une abstraction pour générer A et une autre pour générer B. Avoir deux implémentations vous donne un contrôle plus granulaire, et vous pouvez créer deux stubs. Lorsque vous inversez l'ordre des appels, le code appelle toujours les bonnes abstractions et les mêmes stib, on obtient donc le même résultat final.
Modifier une base de code existante pour introduire des indirections afin de contrôler des valeurs générées aléatoirement n'est pas toujours simple ni nécessaire. Parfois, la valeur générée ne nous importe pas, et nous pouvons tout à fait accepter l'aléatoire tant que nous parvenons à maintenir nos tests déterministes. Et c'est le point de la troisième idée.
Masquer l'aléatoire
Parfois, la valeur aléatoire n'est pas vraiment importante pour le résultat. Si cela ne nous importe pas, nous pouvons l'éliminer de ce qui est vérifié par une assertion dans le test.
L'astuce est que vous n'avez pas l'obligation de faire des assertions basées sur les sorties du système sous test telles quelles. À la place, vous pouvez récupérer ces sorties et les transformer pour ne conserver que les informations dont vous avez besoin pour être suffisamment confiant que vous ne cassez rien.
Lors de la création de tests d'approbation, il est courant d'utiliser un printer, une fonction ou une classe qui transformera les sorties pour les rendre faciles à comprendre. Si une information n'est pas pertinente, le rôle de l'imprimante est de la supprimer et de créer une sortie propre. Vous pouvez alors utiliser cette sortie propre dans l'assertion.
Quoi de plus simple que de supprimer ce dont on ne se soucie pas ?
Tu en as marre de tes tests ?
J’ai créé une formation vidéo qui aide les développeuses et développeurs à améliorer leurs tests automatisés.
Dans cette formation je partage les idées et techniques qui permettent de rendre des tests lents, qui cassent à chaque modification du code et sont incompréhensibles en des tests avec lesquels on a plaisir à travailler.
Parfois, vous ne vous souciez pas de la valeur réelle, mais vous voulez vous assurer qu'une valeur est présente. Au lieu de supprimer la partie aléatoire, vous pouvez la remplacer par autre chose en utilisant un scrubber. Si vous voulez vous assurer qu'un UUID est affiché mais que sa valeur ne vous importe pas, vous pouvez utiliser une expression régulière qui va remplacer tous les UUID par autre chose, comme une chaîne UUID
.
Un autre cas d'utilisation courant est de détecter qu'une valeur est répétée à plusieurs endroits dans la sortie.
Continuons avec l'exemple précédent. Vous ne vous souciez toujours pas de la valeur de l'UUID, mais vous voulez vérifier que l'UUID utilisé dans un lien pour accéder à un produit est bien celui du produit, et non un autre.
Pour cela il faut rendre notre scrubber un peu plus intelligent.
Lorsqu'un UUID est détecté à l'aide d'une regex, le scrubber génère une nouvelle chaîne, habituellement à partir d'une chaîne de base et d'un incrément (UUID_1
). Ensuite, il remplace toutes les occurrences suivantes de cet UUID par la chaîne générée, incrémente le compteur et répète l'opération pour chaque UUID trouvé dans la sortie. De cette façon, même si vous ne pouvez pas prédéterminer le résultat du système, vous pouvez le transformer en quelque chose de déterministe que vous pouvez utiliser comme référence.
Le mieux avec cette solution est qu'elle ne nécessite pas de toucher au système legacy. Vous n'avez pas besoin de vous inquiéter de casser quelque chose en créant les tests. Cette méthode est donc très pratique et relativement rapide pour créer de la confiance. Avec un peu de chance, vous pourriez même travailler avec un langage où les outils d'approval testing viennent avec des scrubbers qui font une partie du travail de nettoyage pour vous.
Comprendre le fonctionnement d'un système utilisant de l'aléatoire et devoir créer un filet de test n'est pas obligatoirement effrayant ou ne doit pas forcément prendre énormément de temps. Avec les trois idées exposées dans cet article, vous devriez être en mesure d'attaquer ce code legacy que vous évitez de refactoriser depuis déjà trop longtemps.
Si vous souhaitez un coup de main pour attaquer un chantier de reprise de code legacy, discutons-en et voyons comment je peux vous aider.