Approval testing : L’art de sécuriser la refonte de son legacy 

Article extrait du n° 100% féminin : Les femmes et la Programmation - Lance-toi! #250

May 18, 2022

Les tests sont le premier client de notre code. Ils nous donnent un feedback quasi immédiat sur le comportement du système testé face à un bug ou une nouvelle fonctionnalité.

Sans tests, la développeuse est privée de ce précieux premier feedback et est exposée à deux risques :  

1- Casser une fonctionnalité clé de la production! 

2- Casser une fonctionnalité clé de la production au pire moment!

 (ex: veille de démo )

Quel que soit son niveau d’expérience, la crainte légitime de causer des régressions est redoublée quand on connaît très peu le code legacy à corriger ( ou à faire évoluer) et que ce dernier comporte peu ou pas de tests. 

Fort heureusement, il existe depuis quelques années déjà des techniques de tests éprouvées pouvant aider à explorer un code non testé en un minimum de temps.

Dans le cadre d’un BBL,  nous avons exploré la façon de sécuriser un code JS ne contenant quasiment pas de tests à l’aide de la technique dite du « Golden Master » 

En partant d’un Kata maison, nous nous attelerons dans cet article à vous présenter cette technique.

“Golden Master” ou “Approval Testing” ou “Snapshot testing” : plusieurs terminologies pour un même concept

Avant d’aller plus loin, nous aimerions revenir rapidement sur les mots et terminologies employées tout au long de cet article.

Bien que le terme “Golden Master” ait servi à populariser cette pratique de test, il est de plus en plus courant de retrouver la même technique sous le terme d’”Approval testing”. 

Quelle différence cela fait-il ? En pratique aucune. Si vous connaissez la technique du Golden Master, vous connaissez la pratique du “Approval testing” et vice-versa.

Au niveau conceptuel, le terme “Golden master” semble porter une charge historique qui contrarie Emily Bache (https://proagile.se/blog/say-approval-testing-instead-of-golden-master) qui exhorte à préférer le terme “Approval testing”.

N’ayant pas d’attachement spécifique pour le terme “Golden Master” et n’ayant pas trouvé une traduction française élégante, nous parlerons aussi bien de tests “Approvals” et d’”Approval testing” que de “Golden Master” pour parler de la technique associée tout au long de cet article 

Principes

Contrairement aux tests de pré-production “classiques” (unitaires, intégration, end-to-end)

qui assurent de la pérennité d’une fonctionnalité donnée, les tests “approvals” visent à faire un état des lieux du code.

 Toute la puissance de cette technique réside dans le fait d’écrire un minimum test permettant de produire un résultat ( textuel ou visuel) montrant les changements enregistrés. 

Il n’y pas de préoccupations liées aux dépendances ni aux sources de données dans ce type de test et encore moins de prise en compte de l’implémentation d’une fonctionnalité.

Par ailleurs, parce qu’il montre  l’état des lieux d’un code donné à un instant précis, ce type de test est nécessairement éphémère car,  la correction d’un bug par exemple change fatalement le comportement initial du logiciel.

Il n’y a pas de logique de préservation d’un comportement du code. 

Mais, à la manière d’un gestionnaire de version, un test “Approval” montre à chaque exécution, les différences entre le résultat d’une exécution initiale  et les  changements intervenus. La développeuse choisit quel changement approuver.

Les tests “Approvals” sont ainsi les seuls types de tests qui doivent être supprimés dès qu’une nouvelle fonctionnalité est implémentée ou un bug est corrigé.

Par ailleurs, il est courant d’associer l’exécution d’ un test “Approval” avec un outil de couverture de code afin de mieux comprendre quelle portion de code est couverte par quels jeux de tests.

Très pratique dans une phase d’acculturation au code legacy, nous montrons dans le point suivant comment démarrer avec l’Approval testing.

Usages

Il existe depuis plusieurs années des outils clefs en main permettant de faire de l’Approval testing. La librairie de référence s’appelle d’ailleurs  “ApprovalTest” et a été développée par Llewewyn Falco

Dans un autre registre, Jest - le framework de test de Facebook - a popularisé la technique de l’approval testing par un brillant recyclage sous le terme de “snapshot testing”.

Cependant, les outils, aussi utiles soient-ils, ont  tendance à mystifier des pratiques pourtant accessibles.

C’est pour permettre à qui veut comprendre le concept derrière le snapshot testing de Jest que nous avons opté, dans le cadre de cet article,  pour une implémentation manuelle. 

Génération manuelle des entrées/sorties

Pour faire un état des lieux pertinent de son code, il est conseillé de procéder en 3 étapes pour toutes fonctionnalités à tester. 

  1. Passez des paramètres d’entrées que vous maîtrisez à l’application et exécutez les tests  progressivement puis, enregistrez la sortie complète dans un fichier.
  2. Créez un test automatisant l’injection des mêmes données dans le système. Le test doit être capable de capturer la sortie du programme (devenue prédictible) et de le comparer avec la sortie de l’étape 1 ci-dessus.
  3. Mesurez la couverture de test. Répéter l’opération jusqu’à ce que la couverture de test soit à près de 100%.

Et voilà ! 

Le cas pratique 

Chez CodeWorks, nous avons créé un kata maison faisant office de legacy sur lequel il est demandé d’assurer la refonte, avant de pouvoir ajouter de nouvelles fonctionnalités. Comme par exemple l’évolution du calcul du score.

 Pour une question ouverte, la personne qui valide les réponses du candidat doit pouvoir ajouter un bonus de 0,5 point, si la réponse est très satisfaisante.

Vous pouvez retrouver ce kata sur notre Github : https://github.com/CodeWorksFrance/kata-technical-workshop

Le fonctionnement du code de production

Il existe une liste de questions à poser à un candidat. 

Chaque question est liée à un langage et à un niveau de difficulté prédéfinis.

<pre>

 [ {

         label: 'Javascript',

         questions: [

             {

                 label: 'How to find the length of the string ?',

                 answer: 'String.length',

                 difficulty: 1

             }

         ]

     },

     {

         label: 'Javascript',

         questions: [

             {

                 label: 'How to return a random number between 0 and 1 ?',

                 answer: 'Math.random()',

                 difficulty: 1

             }

         ]

     }]

</pre>

Suivant le profil du candidat, CodeTest affiche les questions liées aux langages choisis.

Lorsque le candidat a fini de répondre aux questions, un.e CodeWorker doit évaluer les réponses du candidat.

La personne qui procède à l’évaluation n’est pas forcément experte sur les technos sélectionnées.

À ce jour, il n’existe qu’un test unique pour le cas où la personne refuse de répondre aux questions.

Présentation de la stack technique  

Pour construire ce kata, nous avons choisi les technologies suivantes :

  • une interface en lignes de commandes réalisée avec Node.js
  • des questions stockées dans un fichier json
  • le test runner Jest
  • la dépendance prompt-sync pour poser des questions et retourner la réponse saisie. La méthode prompt appartenant à la Web API. Nous voulions avoir le même comportement sous Node.

Notez que nous utilisons Jest comme test runner. Vous pouvez vous demander pourquoi nous n’utilisons tout simplement pas les snapshots de Jest. C’est un choix délibéré. Le snapshot est un exemple de “golden master” (https://alisterbscott.com/2020/10/28/api-approval-testing-using-jest/). Nous avons choisi d’illustrer la redirection et la sauvegarde de la sortie attendue. Plutôt que de faire une démonstration d’un outil. Ainsi, nous avons la main sur toute l’implémentation de notre golden master. C’est d’ailleurs de cette façon que, moi, Romy, je l’ai personnellement découvert. À l’époque, c’était dans l’écosystème Java.

Pour lancer tous les tests en continu, sous IntelliJ ou Webstorm, vous pouvez exécuter la commande `npm start` dans votre terminal. Ou utiliser la configuration suivante:

passez `--watchAll` dans les Jest options et sélectionnez le bouton radio `All tests`

[Fig.1 - Configuration d’exécution automatique de tous les tests, fichier: 04-IntelliJ-configuration.png]

Au lancement de l’application 

La commande « npm start» permet de lancer l’application.

Par défaut, le profil du candidat est développeur.se Java.

CodeTest nous souhaite, ensuite la bienvenue et indique que nous aurons 8 questions en Java.

À la question “Are you ready ?”, taper “y” et “Entrée” fait défiler les questions. Taper tout autre caractère et “Entrée” arrête l’application, après indication du score de 0 point.

[Fig.2 - Sortie attendue en production, fichier:  02-CodeTest-avant-refus.png]

La génération de la sortie du code de production

[Fig.3 - Génération de la sortie attendue en production, fichier: 02-CodeTest-apres-sortie-attendue.png]

La génération de la sortie à la demande

Dans le module chargé de générer la sortie à la volée, nous utiliserons des variables dynamiques, en lieu et place des chaînes de caractères en dur.

[Fig.4 - Sortie générée à la demande, fichier: 03-CodeTest-apres-sortie-a-la-demande.png]

Notez que dans les deux cas, nous appelons le code de production, avec l’instruction `runCodeTest()`.

La comparaison des deux sorties

[Fig.5 - Implémentation du golden master, fichier: 01-CodeTest-apres-golden-master.png]

C’est le seul test que nous rédigeons dans ce fichier. Son unique responsabilité consiste à comparer la sortie en production et celle qui est générée à la volée.

La sécurisation du code de production

Pour commencer, nous pouvons penser le code de production comme une boîte noire.

Le point d’entrée a pour unique responsabilité le lancement de CodeTest. Une fois son exécution finie, le process Node.js est terminé.

[Fig.6 - Point d’entrée de l’application, fichier: 03-CodeTest-avant-index.png]

[Fig.7 - CodeTestRunner, fichier: 04-CodeTest-avant-codeTestRunner.png]

Maintenant que tout est en place, nous allons faire échouer le test, pour nous assurer qu’il joue bien son rôle.

Quand je remplace ‘SQL’ par ‘Java’, ligne 4 dans codeTestRunner.js, le test échoue :

La valeur attendue contient ‘Java’ et non ‘SQL’, qui est contenue dans la sortie en production.

[Fig.8 - Echec du test, après modification du code de production, fichier: 05-Test-echec-apres-modif.png]

[Fig.9 - TechnicalWorshop, fichier:  05-CodeTest-avant-technicalWorkshop.png]

Le golden master va pouvoir pleinement entrer en action avec les tests de caractérisation. Ces tests exploratoires vont venir documenter le code.

Place à l’exploration!

Golden master + Tests de caractérisation = combo parfait 

. Le golden master sécurise la sortie attendue d’une entrée donnée. Quand les tests de caractérisation, eux, viennent révéler le comportement du code de production. À mesure que nous rédigeons des tests de caractérisation, le résultat actuel nous donne le résultat attendu, qui fera passer le test en cours. 

Un premier test de caractérisation pourrait nous révéler ce qui se passe quand `TECHNICAL_WORKSHOP.addCat('SQL')` est appelée ligne 4 dans codeTestRunner.js.

[Fig.10 - Echec du test, fichier: 06-Test-echec-technical-worshop-addCat.png]

[Fig.11 - Echec du test, révélation du retour, fichier: 07-Test-echec-technical-worshop-retour-addCat.png]

Le compilateur et le résultat du test nous indiquent que cette fonction ne renvoie rien. Nous mettons à jour notre test avec cette nouvelle information.

[Fig.12 - Succès du test, fichier: 08-Test-succes-technical-worshop-addCat.png]

[Fig.13 - Succès du test, révélation du retour, fichier: 09-Test-succes-technical-worshop-retour-addCat.png]

Les tests passent.  Par la même occasion, nous retrouvons le log `Adding SQL in categories`, comme au lancement de l’application. Il ne reste plus qu’à s’occuper de l’avertissement du compilateur. La fonction ne retournant rien, le compilateur nous avertit qu’un tel test n’a pas lieu d’être.

[Fig.14 - Dans technicalWorshop.js, fichier: 10-Technical-worshop-déclaration-addCat.png]

Notez qu’après l’affichage du log, un tableau `cat` est alimenté. Il convient donc mieux de vérifier la longueur de `cat`.

[Fig.15 - Echec du test, après évolution, fichier: 11-Test-echec-technical-worshop-cat.png]

[Fig.16 - Echec du test, après évolution, fichier: 12-Test-echec-technical-worshop-cat.png]

Étonnement, alors qu’un label est ajouté ligne 15 dans `technicalWorkshop.js`, la longueur du tableau est ou reste égale à 0.

[Fig.17 - Succès du test, après évolution, fichier: 13-Test-succes-technical-worshop-cat.png]

Poursuivons l’exploration, pour en apprendre un peu plus.

[Fig.18 - Succès du test, après évolution, fichier: 14-Test-succes-technical-worshop-candidate.png]

[Fig.19 - Succès du test, après évolution, fichier: 15-Test-succes-technical-worshop-loadQByCat.png]

L’ensemble des tests, en combinaison avec le golden master, nous permettent de refactorer avec assurance.

[Fig.20 - Début refactoring, après renommage `loadQuestionsByCategory`, fichier: 16-Test-succes-technical-worshop-loadQuestionsByCategory.png]

Nous vous invitons maintenant à poursuivre l’exploration. Le Métier aimerait revoir le calcul du score. Lorsque le.la candidat.e donne 3 bonnes réponses successives, le score est augmenté d’un bonus de 3 points. Vous avez maintenant toutes les connaissances pour mener à bien ce refactoring et mettre le nouveau calcul du score en production.

Conclusion

À travers CodeTest, nous avons pu voir une manière d’implémenter manuellement des tests dit “Approvals”. Nous sommes volontairement allées plus loin avec l’ajout de tests de caractérisation pour montrer la puissance que le Golden master et les tests de caractérisation peuvent vous apporter lors d’une refonte de code legacy. . 

. N’hésitez pas à poursuivre l’exercice pour vous entraîner. Et livrer la nouvelle évolution, dans un cadre sécurisé et reproductible.

CodeWorks, un modèle d'ESN qui agit pour plus de justice sociale.

Notre Manifeste est le garant des droits et devoirs de chaque CodeWorker et des engagements que CodeWorks a vis-à-vis de chaque membre.
Il se veut réaliste, implémenté, partagé et inscrit dans une démarche d'amélioration continue.

Rejoins-nous !

Tu veux partager tes connaissances et ton temps avec des pairs empathiques, incarner une vision commune de l'excellence logicielle et participer activement à un modèle d'entreprise alternatif, rejoins-nous.