Intro
Et si on parlait test ?
La qualité d’une application va de pair avec une application sans bug. Pour les détecter, rien de tel que des tests. Mais ça ne se passe pas toujours bien...
Les Tests unitaires
Le principe est tout simple : j’écris du code, je le teste. Et pour que cela soit le plus simple possible, on mock tous les services externes afin de maîtriser au mieux les cas et ne pas être impactés par des évolutions futures. On a tous en tête un framework qui va bien dans nos technos favorites.
C’est basique mais il faut le rappeler : les TU sont biaisés !
Au final, j’écris les tests en fonction de ce que j’ai codé et s’il y a un bug… Ça devient un cas normal d’utilisation et on dira au chef
« Je ne comprends pas, pourtant j’ai tout bien testé... »
Et on le sait tous, tester c’est douter. Mais finalement, douter ça a du bon.
Nous ne sommes pas éternels, un jour nous quitterons enfin ce projet legacy sur lequel une armée de développeurs est passée, fini le patron qui ne vous considère pas à votre juste valeur et vous refuse cette augmentation tant désirée, ou nous irons enfin élever ces fameuses chèvres dans le Larzac.
Mais surtout, notre mémoire n’est pas infaillible. Vous vous souvenez de ce bout de code pondu il y a 1 an un vendredi soir à 18 heures pour fixer la prod juste avant le week-end ? Non ? Mince, il faut le faire évoluer, et bien entendu pas de test parce que c’était urgent… et en plus c’était vendredi. Du coup vous le savez d’avance, spéléologie au programme…
« C’est quoi cette variable avec ce nom à coucher dehors ? Mais pourquoi cette méthode fait tout sauf ce pourquoi elle a été nommée ? »
Ça y est, on y est, vous le sentez bien le festival du bug ? Si seulement il y avait un moyen de vérifier que l’on n'a pas touché un truc qu’il ne fallait pas… C’est là que les tests sont utiles, et si vous ne doutez pas de vous maintenant, doutez de votre futur vous qui devra faire un truc en urgence absolu un vendredi soir juste avant de partir en vacances.
Le saint graal : le code coverage
Ah, la métrique absolue de toute quality gate qui se respecte : on mesure le nombre de lignes testées et on se fixe un ratio à respecter. Cela va garantir que votre application est fiable et robuste et, en réalité, ça ne veut absolument rien dire et ça n’est gage de rien du tout.
« Comment ça, gage de rien ? »
Et oui, petite anecdote qui m’est arrivée et qui vous fera prendre conscience de cette réalité :
Donc oui, je persiste : le code coverage seul est une métrique inutile, conformément à la loi de Goodhart.
“C’est quoi cette loi?”
Si on définit un critère comme signe de qualité, alors les gens vont naturellement adopter des stratégies pour l’optimiser de façon artificielle ; dans notre cas, on va tester des getters/setters par exemple. Il vaut donc mieux moins de tests mais des vrais tests, plutôt que plein de tests sur du code inutile, comme des getters/setters, ou sans rien vérifier. Attention, je ne dis pas de ne pas tester, je dis qu’il faut choisir ses combats, et si, en définitive, avoir un code coverage plus élevé revient à faire du test sans réel pertinence, alors il faut chercher une autre solution, comme exclure les DTO et les classes de configuration du scope, par exemple, pour se concentrer sur ce qui apporte vraiment de la plus value. Ou bien il faut changer d’approche…
Le Test Driven Developpement ou TDD
Tester, c’est bien, mais bien tester, c’est mieux.
Le mantra est simple :
1. J'écris mon test
2. Je lance les tests -> il échoue
3. J'écris le minimum de code pour le faire passer au vert
4. Je refacto mon code
5. Je recommenceEn faisant de la sorte, vous devez réfléchir à ce que vous voulez faire et comment vous voulez le faire. Cela aide grandement à améliorer votre approche du sujet, la conception de votre solution, mais également ce que votre code doit ou ne doit pas faire.
La théorie est belle, mais dans la pratique… sur un projet avec peu de tests, c’est vite compliqué et on arrive vite à devoir tester toute une méthode que l’on ne connaît pas, en testant à l’ancienne puis en ajoutant nos tests. C’est chronophage, pas hyper fiable et pas très valorisant.
Et même sur un projet tout nouveau tout beau, le mettre en place reste un chemin de croix pour faire adhérer l’équipe, ou plutôt imposer ce choix en refusant toute les pull requests non couvertes par des TU à minima, parce qu’à force de diplomatie et de débats interminables, on a laissé la démocratie au placard. Les habitudes sont tenaces, le besoin était pour hier, et au final j’écris plus de tests que de code donc je perds du temps. Les plus rusés d’entre nous aurons également compris que Git permet de faire croire que l’on a fait du TDD alors que pas du tout, ce qui fausse totalement la démarche mais le contrat semble respecté.
On se posera également la question de l’utilité d’un test sur une méthode passe-plat entre le controller et la couche DAO sans intelligence :
“Ce bout de code qui applique juste une regex, je dois le tester aussi ?”
Faire du TDD est une très bonne chose, mais cela demande de la discipline et tout le monde n’est pas prêt à changer ses habitudes. Pourtant, le jeu en vaut la chandelle : le coverage est proche des 100% avec uniquement des tests ayant une vraie utilité.
Tests unitaires ou Tests d’intégration ?
Comme je l’ai déjà évoqué , le TU c’est bien, mais pas top. On peut donc step up tout ça en passant aux tests d’intégration. Mais c’est quoi, au juste ?
- Si vous parlez avec un QA, un test d’intégration se fait sur un environnement réel avec de vraies applications et une vraie base.
- D’un point de vue dev, un test d’intégration c’est le fait de tester votre application en mode boîte noire : le test se borne à appeler l’API et vérifier le retour. Tous les entrants, par contre, sont maîtrisés pour contrôler les cas. Nous allons nous concentrer sur cette définition des tests d’intégration.
Ce type de test semble hyper compliqué car on utilise pas mal de gros mots qui font peur, mais le principe est en réalité tout simple. Une application a besoin de sa base de données et/ou de consommer des services externes pour fonctionner, on va donc lui fournir tout ça… ou presque.
- Pour la base de données, des solutions type Test Containers sont top : vous démarrez une vraie base dockerisée et la configuration est injecté directement dans l’application. Il est toutefois possible de se rabattre sur des bases embarquées, le souci est que pour une base relationnelle par exemple, on reste sur une base H2 ; si vous utilisez une spécificité de votre SGBD, il n’est pas garanti que cela fonctionne.
- Pour les appels externes, on va tricher, d’où le “presque”. Wiremock permet d’intercepter les appels sortants et de fournir un retour que nous définissons. Plutôt pratique, car on peut de ce fait simuler n’importe quel cas, y compris des timeout ou des erreurs techniques.
- Pour les appels entrants, mockMVC sera votre fidèle allié : il simulera un appel HTTP entrant et vous fournira la réponse que vous pourrez torturer selon votre bon vouloir.
Une fois la stack mise en place, on peut tester l’application sans avoir aucune idée de l’architecture logicielle interne. Par contre, il faut démarrer une application, donc faites bien attention à ce que celle-ci ne soit pas redémarrée en boucle.
Ces tests sont plus proche de ce que les utilisateurs auront comme expérience. On peut donc facilement reproduire un cas de prod qui bug. ou tester selon de vrais cas métier…
TDD ou ATDD ?
Commençons déjà par expliquer brièvement les deux avant d’aller plus loin.
Le Test Driven Development, ou TDD pour les intimes, a déjà été présenté. Pour rappel Le TDD suit la rythmique suivante :
1. J’écris un test
2. Je lance les tests et mon test plante
3. J’écris le minimum de code pour faire passer le test au vert
4. Je refacto la méthode pour avoir un meilleur code (plus lisible, plus performant, plus maintenable, en factorisant le code dupliqué…)
5. Si j’ai encore des cas à couvrir, je recommenceLe but : ne plus avoir de code non testé.
On a déjà évoqué les avantages et inconvénients. Sauf un : le TDD se base sur ce que le développeur veut coder, et donc sur ce qu’il a compris du besoin. Si votre PO ou un tech lead n’a pas jugé nécessaire de détailler le ticket car le besoin était évident pour lui, et que le ticket date d’il y a 6 mois… bonjour la cata. Tout est testé, oui, mais rien ne marche pas comme il faut !
C’est là que l’Acceptance Test Driven Development entre en scène. Ici, pas de boucle mais un gros travail préparatoire. Votre PO favori devra écrire les critères d’acceptance explicitement et votre QA viendra agrémenter le tout de cas plus épicés pour le plaisir des papilles. Ensuite, vous dégainez vos plus beaux TI pour coder tous cas avant de coder la fonctionnalité.
Oui, c’est beaucoup de boulot en amont, mais le jeu en vaut la chandelle car Michel saura exactement ce qui est attendu : plus d’interprétation douteuse. Du coup, la qualité du projet n’en sera qu’augmentée. Cerise sur le gâteau : on a tout un jeu de TNR joué en automatique, plus d’effet de bord découvert en prod (ou en tout cas moins). Et oui si le PO, le QA et le dev ne l’avaient pas anticipé, ce n’est pas testé. Même ChatGPT fait des erreurs, alors nous, pauvres humains…
Pour essayer d’anticiper un maximum les cas, il faut s’appuyer sur d’autres méthodologies existantes telles que les Three Amigos, ajouter au Definition Of Ready la définition des tests, de l’event modeling… d’autres solutions existent, à vous de trouver celles qui seront les plus adaptées à vos cas d’usage.
Et c’est tout ?
Maintenant que l’on a tout compris sur les tests et les pièges, qu’est ce que l’on peut faire pour aller plus loin ? Parce que si on fait de l’ATDD, l’investissement est quand même conséquent, ça serait bien d’en tirer plus que juste de la qualité… une doc peut être ? Genre on norme nos tests et bim, ça génère une doc technique de l’application avec tous les cas couverts et les Règles de Gestion métier. Pratique pour les études d’impact ou l’onboarding des nouveaux collaborateurs. Et comme c’est généré à partir du code, elle est maintenue et à jour.
Cucumber ou Jgiven, par exemple, peuvent vous sauver la vie dans ce cas. Ils imposent une syntaxe Gherkin pour écrire les tests, donc mieux vaut le mettre en place en début de projet pour éviter une grosse refacto, mais derrière on dispose d’un rapport html que l’on peut partager.
Allure permet aussi de générer ce type de documentation sur base de JUnit.
Une autre voie consisterait aussi à tester nos tests. Cela semble fou mais oui, si je peux m’assurer que mes tests sont pertinents et n’oublient pas de cas, c’est plutôt sécurisant. Encore une fois, une solution existe : les mutations test. Le principe est simple : imaginez un stagiaire qui forcerait tout vos if à false, un par un, et qui lancerai les tests a chaque fois. Si au moins un test tombe en erreur, alors c’est que c’est testé, sinon il manque un test. C’est ce que font, entre autres, les mutations tests, parmi plein d’autres variations. Le revers de la médaille n’est pas anodin non plus : le temps de compilation va exploser en fonction de la complexité du projet et des règles que vous voudrez tester. Il faut donc bien réfléchir avant de les mettre aveuglément.
Conclusion
Pour conclure, les tests sont une part importante de la démarche qualité d’une application. Mais leur utilité peut aller bien au-delà de simplement s’assurer du bon fonctionnement de l’application. Ils ne doivent donc pas être pris à la légère, ils sont loin d’être du temps perdu et vont même vous sauver le vie à plusieurs reprises. Chaque solution a ses avantages mais également ses contreparties, à vous de choisir les solutions les plus adaptées à votre équipe, votre contexte projet mais aussi votre niveau de compétence. Ne visez pas l’ATDD d’emblée sur un projet legacy avec une couverture de test ridicule par exemple, dans ce cas privilégiez la mise en place de tests d’intégration basés sur les cas métier connus. Ils seront plus simples à identifier, cela ne nécessite pas de connaître la base de code de façon approfondie et la couverture de code sera déjà amplement satisfaisante. Respectez également la pyramide des tests : ne testez pas deux fois la même chose avec deux méthodologies différentes. Si vous mettez des tests d’intégration, ne les testez pas en test unitaire mais orientez-les plus vers des cas techniques bien spécifiques, tels que des exceptions Java ou du code très tricky.


_ZO6xDo.webp)