On en a parlé dans le chapitre sur les environnements, les tests automatiques sont au coeur du développement logiciel moderne. Mais concrètement, de quoi parle-t-on ?
Un test automatique, c'est du code qui vérifie que d'autre code fonctionne correctement. Au lieu de cliquer manuellement sur chaque bouton du site pour vérifier que tout marche, les développeurs écrivent des programmes qui font ces vérifications à leur place.
Une fois écrits, ces tests peuvent être relancés des dizaines de fois par jour sans effort, et le sont automatiquement à chaque modification du code par la chaîne d'intégration continue. Ils sont la première ligne de défense contre les bugs et les régressions, ce mot désignant une fonctionnalité qui marchait hier et que la modification d'aujourd'hui vient de casser sans que personne ne l'ait voulu. C'est le bénéfice principal des tests, bien avant la découverte de nouveaux bugs.
Rien à voir avec les tests A/B, qui portent presque le même nom et comparent deux versions d'une page auprès de vrais visiteurs. Ici, on parle de code qui vérifie du code, sans aucun utilisateur dans la boucle.
Les différents niveaux de tests
On distingue classiquement trois niveaux, du plus petit au plus large, chacun vérifiant une chose différente.
Les tests unitaires
Ils testent une seule petite brique du code, de manière isolée.
Prenons une fonction qui calcule un prix TTC. Mon test unitaire vérifie que
calculerPrixTTC(100, 20) retourne bien 120, et que
calculerPrixTTC(0, 20) retourne 0. C'est rapide, ciblé, et si ça casse,
on sait immédiatement où est le problème.
Les tests unitaires sont les plus nombreux dans un projet. Un bon projet peut en avoir des centaines, voire des milliers. Ils s'exécutent en quelques secondes.
Les tests fonctionnels (ou d'intégration)
Ils ne testent plus une brique isolée mais plusieurs qui travaillent ensemble.
Cette fois, je simule un utilisateur qui se connecte, ajoute un produit au panier, entre ses coordonnées bancaires et valide le paiement. Le test vérifie que la commande est bien créée, que le stock est mis à jour et que l'email de confirmation est envoyé.
Ces tests sont plus lents car ils sollicitent une bonne partie du système, mais ils vérifient que
les pièces du puzzle s'assemblent correctement.
Un détail compte ici. Le test n'appelle pas la vraie banque ni le vrai transporteur, il les remplace par des
faux qui répondent ce qu'on leur demande de répondre, des bouchons. C'est ce qui rend le test rapide et
reproductible, et c'est aussi ce qui explique qu'un test puisse passer alors que la vraie intégration échoue,
parce que le partenaire a changé quelque chose que le bouchon ignore.
Les tests de bout en bout
Dernier niveau, le plus proche du réel. Un robot pilote un vrai navigateur sur une vraie version du site, clique sur les boutons et remplit les formulaires comme le ferait un client.
On dit aussi "end-to-end", souvent abrégé E2E. Ce sont les tests les plus convaincants, puisqu'ils vérifient exactement ce que l'utilisateur vivra. Ce sont aussi les plus lents, les plus coûteux à maintenir, et ceux qui échouent le plus souvent pour de mauvaises raisons, un bouton déplacé ou une page un peu longue à charger.
Les autres familles de tests
Ces trois niveaux vérifient que le code fait ce qu'on attend. D'autres tests vérifient qu'il le fait dans de bonnes conditions, et tu les croiseras sous ces noms.
- Les tests de charge : on simule des milliers de visiteurs simultanés pour voir à quel moment le système plie. Indispensable avant une opération commerciale, et lié au chapitre sur la performance.
- Les tests de sécurité : un outil ou un spécialiste tente de forcer les portes décrites dans le chapitre sur la sécurité des API.
- Les tests d'accessibilité : automatisés pour une partie, humains pour le reste, ils vérifient les critères du chapitre sur l'accessibilité.
- La recette : ce n'est pas un test automatique, c'est toi. Vérifier à la main, sur la préproduction, que la fonctionnalité répond au besoin. Les tests automatiques garantissent que le code fait ce que le développeur a compris, la recette garantit que le développeur a compris la bonne chose.
Aucun de ces tests ne tourne sur la vraie base clients. On utilise des jeux de données inventés ou anonymisés, et des comptes de paiement en mode bac à sable, qui simulent une transaction sans débiter personne. Tester avec de vraies données personnelles est une faute au regard du RGPD, et tester avec de vraies cartes bancaires finit toujours mal.
Le BDD : décrire le comportement attendu
Le Behavior Driven Development (BDD) est une approche qui rapproche les développeurs des équipes métier. Les critères de validation d'une fonctionnalité y sont écrits dans un langage compréhensible par tout le monde, pas seulement par les développeurs.
Ce langage s'appelle Gherkin, et l'outil qui l'a rendu populaire s'appelle Cucumber. La structure est volontairement simple :
Étant donné un client avec un abonnement premium
Quand il ajoute un produit à 50€ au panier
Alors une réduction de 10% est appliquée
Et le total affiché est de 45€Ce scénario est écrit en collaboration avec le demandeur (chef de produit, product owner...). Le développeur le transforme ensuite en test automatique. Si le test passe, la fonctionnalité est validée.
L'intérêt du BDD, c'est que les critères de validation sont clairs pour tout le monde avant le début du développement. Ça évite les malentendus et les allers-retours.
Le TDD : tester d'abord, coder ensuite
Le Test Driven Development (TDD) pousse la logique encore plus loin, puisqu'on écrit le test avant le code.
Le cycle est simple :
- Rouge : écrire un test qui échoue (le code n'existe pas encore).
- Vert : écrire le minimum de code pour faire passer le test.
- Refactoring : améliorer le code tout en s'assurant que le test passe toujours.
Ce cycle se répète en boucle, test par test, fonctionnalité par fonctionnalité.
Le TDD est un sujet qui divise la communauté. Ses partisans affirment qu'il produit un code mieux conçu, plus testable et plus fiable. Ses détracteurs trouvent que c'est trop rigide et que ça ralentit le développement.
De mon expérience, la vérité est entre les deux. Le TDD est un excellent exercice pour les développeurs en formation. Il force à réfléchir au "quoi" avant le "comment". En pratique, les développeurs expérimentés alternent entre TDD et approche classique selon la complexité du sujet.
La pyramide des tests
Ces trois niveaux ne se valent pas en quantité. L'image qui circule depuis une quinzaine d'années, popularisée par Mike Cohn, est celle d'une pyramide :
- Base (large) : beaucoup de tests unitaires, rapides et ciblés.
- Milieu : moins de tests d'intégration, qui vérifient les assemblages.
- Sommet (étroit) : très peu de tests de bout en bout, qui rejouent les quelques parcours vraiment critiques, typiquement l'inscription et le paiement.
L'erreur classique consiste à inverser la pyramide, en n'ayant que des tests de bout en bout. Ils sont lents, fragiles et quand ils échouent, on ne sait pas d'où vient le problème.
Ce que les tests coûtent, et ce qu'ils ne garantissent pas
Écrire des tests prend du temps, parfois autant que la fonctionnalité elle-même. C'est la raison pour laquelle
une estimation double quand on demande que le code soit testé, et ce n'est pas du zèle. Les tests se
maintiennent aussi, chaque évolution du produit en casse quelques-uns qu'il faut mettre à jour.
Ils peuvent également mentir. Un test dit fragile échoue une fois sur dix sans raison, parce qu'un délai réseau
a varié ou qu'un bouton s'est affiché trop tard. L'équipe finit par relancer sans regarder, puis par ignorer
les échecs, et le jour où un vrai bug se cache derrière, personne ne le voit.
Tu entendras enfin parler de couverture de code, un pourcentage qui mesure la part du code traversée par au moins un test. C'est un indicateur utile pour repérer ce qui n'est jamais vérifié, et un très mauvais objectif. Une couverture de cent pour cent signifie que chaque ligne a été exécutée pendant un test, pas qu'elle a été vérifiée, et encore moins qu'elle est juste. Une équipe à qui l'on impose un chiffre finit par écrire des tests qui ne testent rien.
Un projet sans tests, c'est un avion sans instruments de bord. Ça vole, mais personne n'ose toucher à quoi que ce soit de peur que tout s'effondre. Les mises en production deviennent stressantes, et les bugs sont découverts par les utilisateurs au lieu des développeurs.