Avant les nouveautés, des tests (beaucoup de tests)
Une application de gestion interne à mettre à jour et à enrichir. J'ai commencé par écrire un peu plus de 400 tests. Ils ont trouvé des bugs que personne n'avait vus, dont un qui supprimait des achats sans prévenir.
Il y a quelques semaines, une entreprise du bâtiment m'a confié son application de gestion interne. Elle tourne chez eux depuis des années : achats, chantiers, pointages, congés, documents. Tout le monde l'ouvre le matin et personne n'y pense tant qu'elle marche.
La demande tenait en deux lignes : mettre le socle à jour (une montée de version majeure du framework) et ajouter quelques outils. J'avoue que la tentation, c'est d'attaquer par les nouveautés, parce que c'est ce qu'on montre en réunion. Sauf que l'application n'avait aucun test, et qu'une montée de version majeure touche à tout en même temps. Sans filet, j'aurais découvert les dégâts par les utilisateurs. J'ai donc commencé par là.
Un peu plus de 400 tests
Avant la première ligne de nouveauté, j'ai donc écrit un peu plus de 400 tests. Ils tournent sur une base MariaDB dédiée, reconstruite à chaque lancement, et chaque test s'exécute dans une transaction annulée à la fin. Les e-mails, les traitements en arrière-plan et les appels à des services externes sont neutralisés (personne n'a envie de recevoir 400 notifications de congé un mardi matin). Un test ne laisse rien derrière lui et ne dépend pas de celui d'avant.
Il y en a de quatre sortes :
- l'affichage : chaque page s'affiche pour chaque profil d'utilisateur, et les accès interdits sont bien refusés ;
- les parcours : des scénarios complets à travers les vrais formulaires (un achat, un congé, un pointage, un export) ;
- les règles métier, testées une par une ;
- et les garde-fous, qui échouent si une page ou un outil n'est couvert par aucun test.
Le test qui surveille les autres
Ce dernier point, c'est celui dont je suis le plus content. Une suite de tests, en général, on la laisse mourir à petit feu : on ajoute une page, on oublie de la tester, puis une autre, et au bout de six mois la couverture ne veut plus dire grand-chose. Je parle d'expérience (j'ai été ce développeur plus d'une fois).
Le garde-fou liste tout ce que l'application expose, le compare avec ce que les tests visitent, et râle s'il manque quelqu'un. En gros, ça donne ça :
$exposed = $this->allRoutesOfTheApplication();
$covered = $this->routesVisitedByTheTests();
// Every exposed route must be visited by at least one test.
self::assertSame([], array_diff($exposed, $covered));
Ajouter une page sans test fait maintenant échouer la suite. Je n'ai plus besoin d'y penser, et le prochain développeur non plus.
Ce que les tests ont trouvé
Une quinzaine d'anomalies ont été corrigées pendant ces travaux, et plusieurs ont été trouvées par les tests plutôt que par quelqu'un devant son écran. Quelques exemples :
- l'export Excel mensuel des pointages échouait à chaque fois depuis une précédente montée de version ;
- les alertes d'expiration d'un document partaient aussi pour ses anciennes versions, déjà remplacées.
Rien de spectaculaire, mais chacune produisait un chiffre un peu faux ou un e-mail en trop, que quelqu'un rattrapait ensuite à la main sans savoir d'où ça venait. Et comme le souci était dans l'outil, il revenait tous les mois.
Et puis il y a eu celle-là, que j'ai relue deux fois pour être sûr (petit moment de solitude) : supprimer une catégorie d'achat supprimait aussi, sans prévenir, tous les achats qui y étaient rattachés, avec leurs paiements, leurs pièces jointes et leurs commentaires. La base de données faisait exactement ce qu'on lui avait demandé : une suppression en cascade, sans doute bien pratique le jour où elle a été posée.
C'est corrigé. Une catégorie, un fournisseur ou un chantier encore utilisé ne peut plus être supprimé, et l'écran explique pourquoi à la place du bouton. Un refus sans explication, l'utilisateur le prend pour un bug, alors autant lui dire.
Les droits, écran par écran
J'ai appliqué la même méthode aux accès. J'ai fait un audit écran par écran, profil par profil, et j'ai refermé ce qui était trop ouvert ; les boutons correspondants sont masqués pour ceux qui n'y ont pas droit. Ensuite, j'ai transformé l'audit en tests : chaque page est visitée avec chaque profil, et un accès qui devrait être refusé et qui passe fait échouer la suite. Comme ça, l'audit se refait tout seul à chaque livraison.
Je ne détaillerai pas ce qu'il a trouvé. Tout est corrigé, mais ce genre de liste intéresse surtout ceux qui cherchent la même chose ailleurs (et je préfère ne pas leur mâcher le travail).
Une commande pour tout vérifier
Des tests qu'on lance quand on y pense, on finit par ne plus les lancer (je plaide coupable). Tout passe donc par une seule commande, composer qa, qui enchaîne :
- le formatage du code et l'analyse statique ;
- la vérification des templates, de la configuration et des traductions ;
- les tests ;
- les migrations de base de données, jouées dans les deux sens : montée, descente, remontée ;
- la compilation des assets ;
- l'audit de sécurité des dépendances.
Elle tourne automatiquement avant chaque mise en production, et si elle échoue, la mise en production n'a pas lieu.
Les migrations, je les joue dans les deux sens parce qu'une migration qui monte mais ne sait pas redescendre, on ne s'en rend compte que le jour où il faut revenir en arrière en production. Et ce jour-là, on a déjà assez de soucis comme ça. Les rejouer à chaque fois prend quelques secondes.
Deux derniers réglages. L'analyse statique n'a plus de baseline, cette liste d'erreurs tolérées qu'on se promet toujours de résorber un jour : je l'ai résorbée, il n'en reste aucune. Et le script de déploiement s'arrête maintenant à la première erreur, au lieu de continuer sur un serveur à moitié mis à jour.
Et après
Tout ça ne se voit pas à l'écran, mais c'est ce qui m'a permis de livrer le reste en deux jours sans stresser. Dans le prochain billet, je raconte la nouveauté qui a le plus changé l'application : la rendre pilotable par un assistant IA, sans lui ouvrir de porte dérobée.