Premiers pas avec Laravel Dusk

publié le 5 janv. 2023

Quand l'application AWKN a eu 3 ans, nous avons constaté que des problèmes revenaient de plus en plus souvent. Le temps de "livraison" d'une fonctionnalité s'allongeait, du fait principalement de l'allongement de la phase de "recette" avant livraison. Pire, certains bugs n'étaient pas vus et ce sont nos clients qui nous les signalaient 😬...

Bref, nous étions en train de creuser notre dette technique. Pas cool.

La dette technique, c'est quoi ?

Il existe de nombreuses théories et définitions à ce sujet, mais pour aller à l'essentiel, la dette technique est une métaphore pour illustrer le travail ET le coût supplémentaires qui résultent de la prise de raccourcis à court terme lors du développement initial. Pour faire une analogie, quand vous construisez une maison, vous pouvez utiliser les meilleurs matériaux, les assembler avec soin, prendre le temps de laisser sécher les murs, et entretenir votre maison en continu. Ou vous pouvez choisir des matériaux un peu moins bons, les assembler à la hâte, et ne plus rien faire pendant 10 ans. Et là les problèmes commencent. En informatique, c'est pareil, sauf qu'après à peine deux ans, le vernis commence à craquer...

Comme une dette financière, la dette technique accumule des "intérêts" sous forme de complexité accrue, de maintenance plus difficile et de risques plus élevés d'erreurs ou de défaillances dans le futur. Si elle n'est pas gérée, la dette technique va ralentir le développement et augmenter les coûts.

A mon avis, la dette technique est le défi le plus difficile à relever dans le monde du développement applicatif, mais aussi le plus rentable quand on le relève. Je ne compte plus le nombre de fois où j'ai vu des équipes informatiques doubler de taille à cause d'une dette technique devenue incontrôlable. Peut-être aussi votre service informatique vous a-t-il déjà annoncé qu'une nouvelle fonctionnalité nécessitait '1 mois de développement et 1 mois de tests' ?

Un investissement à haut rendement : automatiser les tests

Dans notre cas, il était clair que la dette technique nous obligeait à passer de plus en plus de temps à tester et retester l'application, puis à gérer des régressions en urgence.

Une idée s'imposait : automatiser nos tests applicatifs.

Nous avons mis environ 1 mois à mettre en place des tests automatisés pour couvrir tous les cas de figure de toute l'application de l'époque. Ensuite bien sûr il fallait garder cette dynamique et accepter de passer quelques temps (environ 1 jour par mois) pour maintenir et ajouter des tests suite au développement de nouvelles fonctionnalités.

Cet investissement s'est révélé extrêmement rentable. Aujourd'hui à chaque fois que nous développons quelque chose, une nouvelle fonctionnalité ou une amélioration, nous lançons les tests et 15 minutes après (à comparer à 2-3 jours avant...) nous savons si le développement est fiable ou non. Cet investissement à réduit drastiquement nos coûts de maintenance, et je ne peux que le recommander...

Le choix de la suite Laravel Dusk

Nous avons décidé de partir principalement sur une suite de tests "End to End". Il s'agit alors de répliquer ce que ferait un humain : on va ainsi automatiser l'ouverture d'une fenêtre, le fait de taper une URL, le fait de se connecter à son compte, puis de créer un parcours, etc. Le framework PHP que nous utilisons, Laravel, propose justement une solution dédiée à ce genre de tests "End to End". Cette solution s'appelle Dusk.

Les deux principaux avantages de cette technologie sont :

  1. C'est assez "naturel" (bien que fastidieux) de coder des tests

  2. C'est exactement ce que vivra un client réel, et cela permet de rendre extrêmement robustes les tests de "non-regression".

Les désavantages par rapport à des solutions de test unitaires existent cependant :

  • Il y a beaucoup de "subtilités" qu'on découvre dans la douleur, car la documentation est très très légère.

  • Cette suite est beaucoup moins utilisée que PHPUnit et PestPHP, en conséquence le rythme des évolutions tend à ralentir, donnant l'impression que cette suite est délaissée

  • Il y a une forte dépendance à des librairies open-source financées gracieusement par Google et Facebook. Si un jour ces 2 géants cessent de financer ces projets, cela posera inévitablement des problèmes.

Voici ci-dessous quelques conseils pour vous lancer plus rapidement que moi dans cette aventure que je ne regrette absolument pas, cette automatisation des tests ayant réduit à néant les "régressions" sur AWKN et m'apportant une sérénité totale lors des déploiements de nouvelles fonctionnalités.

  • Mettre à jour Chromium

  • Penser les tests par "finalité"

  • Mettre à jour Chromium

  • Loi des 80/20

  • Reproduire la "vraie vie" autant que faire se peut

  • Régler vos tests comme du papier à musique

  • Désactiver le mode Headless

  • Grouper vos tests lors du développement

Mettre à jour Chromium

Dusk utilise Chrome, plus particulièrement le "ChromeDriver", qui est régulièrement mis à jour par google. Mécaniquement, un moment arrivera où les tests ne fonctionneront plus à cause d'un conflit de version entre la version de Chrome installée et celle que souhaite utiliser Dusk. Pour résoudre ce problème, il faut aller sur le site de google et télécharger la dernière version stable correspondant à votre matériel. Ensuite, il faut aller dans le répertoire de Dusk (en général ./vendor/laravel/dusk/bin/) puis vérifier la version qu'on a:

./chromedriver-mac-arm --version //adapter le nom de chromedriver en fonction de ce que Dusk utilise par défaut

Ensuite on va chercher le fichier téléchargé et on le dezip, puis on se remet dans le répertoire et...

cd ./vendor/laravel/dusk/bin/
rm chromedriver-mac-arm //On vire le fichier existant qui plante
mv ~/Downloads/chromedriver chromedriver-mac-arm //On met le nouveau fichier
chmod +x chromedriver-mac-arm //On rend le fichier executable

Ensuite on va exécuter ce fichier depuis le Finder. Soit ça se passe bien, soit votre ordinateur va bloquer l'exécution car étant une application non reconnue... alors vous devrez aller dans les paramètres pour autoriser cette application et normalement tout rentrera dans l'ordre.

Vérifiez que Dusk utilise bien la nouvelle version :

./chromedriver-mac-arm --version

Et c'est tout bon !

Penser les tests par "finalité"

Créer des tests dans Dusk est long et fastidieux. Le processus est très itératif, puisqu'il consiste à scripter chaque fait et geste que ferait un utilisateur. C'est inévitable, lors du développement, d'en oublier, du coup le test ne fonctionne pas, et il faut revoir sa copie, et ainsi de suite.

Hâtez-vous lentement, et sans perdre courage,
Vingt fois sur le métier remettez votre ouvrage,
Polissez-le sans cesse, et le repolissez,
Ajoutez quelquefois, et souvent effacez.

Nicolas Boileau, utilisateur de Dusk ?

Du coup, je conseille vivement de prioriser et sélectionner les tests à automatiser.

Pour prioriser et lotir les développements, j'aime réfléchir par "finalité souhaitée pour l'utilisateur".

Il s'agit de sortir d'une logique de fonctionnalités unitaires à une maille fine (par exemple créer un utilisateur en base de donnée, valider les données du formulaire, etc...) pour une vision plus macro, en se mettant à la place de l'utilisateur final (par exemple : créer un nouveau compte et accéder à son espace personnel).

Cela permet de se rapprocher naturellement d'une vision utilisateur, et c'est aussi plus simple et lisible à maintenir dans le temps.

Cela permet de garder également sa motivation : Au lieu de lister 500 tests et de prendre peur... vous allez lister 10 "finalités", ce qui semblera plus digeste, et vous vous lancerez !

Commencez par lister une dizaine de macro-finalité, puis classez selon vos problématiques actuelles. Ainsi :

  • si vous avez peu de capacité d'exécution, priorisez les tests simples à finaliser

  • si vous avec des bugs qui reviennent fréquemment sur certaines finalités, priorisez celles-ci

  • si vous êtes exposés à des risques de réputation / stabilité / dépendances, partez là-dessus

  • ...

Loi des 80/20

Pour les mêmes raisons, il est inadapté d'utiliser Dusk pour tester tous les cas possibles et inimaginables.

Prenons l'exemple d'un formulaire à remplir. Dans les suites de tests unitaires, on peut tester, pour chaque champs, que les valeurs autorisées peuvent être envoyées, et les valeurs non autorisées seront refusées.

Si vous avez 100 routes dans votre application, il est probable que vous allez avoir autour de 500 tests unitaires pour bien couvrir le périmètre.

Si vous essayez de faire la même chose dans Dusk, vous allez perdre énormément de temps et rendre votre code totalement illisible. Dusk n'est juste pas fait pour cela. Dusk est fait pour vérifier que si l'utilisateur a rempli "tout bien" le formulaire alors il arrive sur une page qui lui dit merci, et si au moins un des champs est mal rempli, alors il ne peut pas valider le formulaire. 1 seul test pour vérifier ces 2 "assertions". C'est tout et c'est suffisant.

Reproduire la "vraie vie" autant que faire se peut

Très souvent dans les tutoriels, on utilise des bases de données en cache (par exemple SQLite) pour les tests, qui présentent plusieurs avantages : on ne touche pas à la vraie base de donnée, les temps de lecture et écriture et destruction sont beaucoup plus rapides, etc.

C'est à mon sens une grosse erreur, d'abord parce que certaines fonctionnalités de MySQL ou PostGreSQL n'existent pas dans SQLite mais aussi parce que cette rapidité n'est pas représentative de la réalité. Je conseille plutôt de créer une base de données dédiées aux tests mais qui a exactement les mêmes caractéristiques que ce que vous avez en production. Cela vous évitera bon nombres de tests faussement positifs (ils passent mais le bug existe en prod) ou faussement négatifs (ils ne passent pas alors que cela fonctionne parfaitement en prod).

Régler les tests comme du papier à musique

Automatiser avec Dusk, c'est simuler l'humain avec un robot, et un robot... c'est idiot. Il faut donc parfois ruser, temporiser et scénariser les actions pour permettre au robot de trouver les éléments à cliquer sur une page.

On pourra ainsi créer un élément (Create) , puis vérifier que l'on y accède (Read), puis le supprimer (Delete), puis recréer (Create) un élément pour le mettre à jour (Update).

On pourra également ajouter des temps d'attente "forcés" pour permettre au JavaScript, côté front-end, de s'exécuter, par exemple pour laisser des boutons "disabled" devenir cliquables. J'ajoute systématiquement 1 seconde d'attente entre chaque clic, et 2 secondes après chaque modification en base de données.

Enfin, si les données sont modifiées via le front-end (javascript) et non via le backend (laravel), il faudra veiller à synchroniser le backend - via les méthodes Laravel fresh() et refresh().

Parfois il faut ajouter une mécanique pour essayer plusieurs fois à la suite un même test avant de le déclarer en échec, car un simple ralentissement du réseau au moment du test peut le faire échouer...

mon_super_test.php
// Ici on va tester deux fois la visite d'une url donnée et son bon fonctionnement
// après un click sur un bouton appelé "addTraining" 

retry($times = 2, function () {
       $this->browse(function ($browser) {
          $browser->visit('/url-a-tester')
			->click('@add-training')
            ->waitFor('#addTraining') // On attend que cet élément apparaisse à l'écran
            ->assertSee('Bravo'); // On vérifie qu'on voit bien le contenu de l'élément.
        });
});

Désactiver le mode Headless

Avant de considérer qu'un test est finalisé ou si votre test bug de manière inexpliquée, vérifiez que le robot fait bien ce que vous croyez que vous lui avez demandé de faire.

Comment ? Super simple, il suffit de désactiver le mode "headless" dans le fichier DuskTestCase.php et quand vous lancerez votre test, la fenêtre s'ouvrira devant vos yeux ébahis et vous verrez toutes les actions s'enchainer.

DuskTestCase
$options = (new ChromeOptions)->addArguments([
        '--disable-gpu',
        // '--headless'. // Ajouter un commentaire ici le temps du développement
    ]);

Grouper les tests lors du travail de développement

Les tests étant longs (c'est-à-dire cela peut prendre plusieurs dizaines de secondes par test puisqu'on simule des actions humaines) à fairer tourner, il faut prendre l'habitude de créer des groupes de type  "group:wip" sur les tests qui sont encore en cours de développement. Pour lancer ensuite ces tests - uniquement ces tests - et s'assurer qu'ils fonctionnent, c'est  :

console
php artisan dusk --group=wip  // Ici on ne lance que les tests appartenant au groupe 'wip'

Voilà, c'est tout pour le moment, bonne automatisation !