Comment j'ai pris plaisir à tester mon code

Est-ce que, toi aussi, tu éprouves une insatisfaction à l’idée de maintenir ton socle de tests automatisés ? Ou tout simplement à écrire de nouveaux tests ? Je jette cette bouteille dans l’espoir que certaines personnes se retrouvent dans mon discours. Lao-Tseu a dit : “Il faut trouver la voie”. Et donc dans l’espoir que tu trouves également la voie de l’amour des tests :D

Quelles étaient donc mes insatisfactions…

…concernant, je pense, l’intégralité des produits auxquels j’ai contribué dans mes précédentes équipes ?

Je ne comprends pas l’organisation des tests. Et donc leur pertinence.
Je ne trouve absolument aucune cohérence entre “tests automatisés” et “équipe de tests”.
Le test demande une certaine maîtrise de l’outillage.
Le test est perçu comme un centre de coût.

Problème 1. Je ne comprends pas l’organisation des tests. Et donc leur pertinence.

C’était pour moi le plus gros frein. Contribuer sur un code comprenant (chiffres quasi-aléatoires) 1000 tests, 25% de couverture du code par les tests et un backlog de bugs monumental. Des régressions devenues habituelles, une gestion de plus de bugs que de fonctionnalités, un chef de projet dédié aux bugs et une équipe de développement qui n’a strictement aucune confiance en son socle de tests automatisés. C’est-à-dire qu’il n’est même pas imaginable et imaginé de déployer une fonctionnalité sans la tester manuellement. On en vient même à tester manuellement d'autres fonctionnalités, au cas où notre code aurait impacté quelque chose qui n’aurait rien à voir.

Problème 2. Je ne trouve absolument aucune cohérence entre “tests automatisés” et “équipe de tests”.

Les tests automatiques écrits par l’équipe de développement sont perçus comme “le bac à sable” des dévs. Ils ne rassurent personne, et ne sont d’ailleurs exploités par personne. Si bien que les campagnes de tests manuels déclenchées par l’équipe de tests ne prennent absolument pas en compte la couverture fonctionnelle déjà automatisée.

On peut tristement arriver à la conclusion : “si l’équipe de tests va passer une semaine à tester la fonctionnalité de fond en comble, à chaque fois qu’il y a la moindre modification sur cette fonctionnalité, pourquoi mettre de l’effort en amont ?”

Problème 3. Le test demande une certaine maîtrise de l’outillage.

Je me suis longuement efforcé d’être pertinent sur la base de code, que ce soit en termes de qualité du code, d’architecture globale de la solution, et de connaissance des librairies et technologies embarquées.

Et il est indéniable que j’étais très peu à l’aise avec la testabilité de mon code. JUnit, Mockito, Cucumber, TestContainers / LocalStack… Ce manque de maîtrise provoquant certainement une envie de “fuir” cette difficulté. Surtout quand on ne croit pas en la pertinence de la chose (cf problèmes 1 et 2).

Problème 4. Le test est perçu comme un centre de coût.

Pour finir et pas des moindres, il est parfois coutume de croiser des ateliers de chiffrage pour chaque fonctionnalité, par exemple en poker planning, où l’on souhaite explicitement afficher la charge de “tests unitaires”.

Parfois même pour une presque-bonne intention : “si on affiche clairement la charge de tests unitaires, les développeurs auront du temps alloué pour les développer, et ils ne pourront pas les oublier”.

La chute de l’histoire est beaucoup trop évidente : que pensez-vous qu’il puisse se passer lorsqu’un chef de projet non sensibilisé à la qualité se rend compte qu’il peut livrer la feature plus rapidement s’il supprime la ligne “tests unitaires” ? Certes cette aberration n’arrive pas fréquemment, mais elle finit par arriver dans un tel contexte.

Le remède.

Ça y est. J’ai fini de lister les anecdotes peu glorieuses… Voyons la vie en rose à présent.

L’organisation des tests et l’organisation du code sont la même chose. Que ce soit lorsqu’on souhaite ajouter du code ou ajouter des tests, il est essentiel de comprendre comment est architecturée la solution. Le miracle de comprendre la structure de notre code et de nos tests s’équilibre probablement entre diverses pratiques :

  1. Le Domain-Driven Design (DDD) et ses contextes bornés (bounded contexts), ou “comment savoir partager les responsabilités métier de manière cohérente”, permettant de découpler la plupart des fonctionnalités, et d’afficher explicitement l’adhérence entre les fonctionnalités ayant une dépendance requise.
  2. L’Architecture Hexagonale ou “comment remettre le métier au centre de tout”, offrant une facilité déconcertante dans le test des règles métiers même complexes, avec de simples tests unitaires. Permettant également de comprendre qu’il faudra probablement un test d'intégration pour s’assurer du bon fonctionnement d’un adaptateur secondaire (repository Spring, connexion FTP, stockage S3, file SQS…). A noter que toute autre forme d’architecture respectant les principes de séparation de responsabilités saura être équivalente en termes d’organisation et de testabilité. L’Architecture Hexagonale a l’avantage d’être exigeante sur ce sujet.
  3. La Pyramide de Tests présentant les différents types de test, permettant de choisir le plus pertinent en fonction de l’intention souhaitée (voir cet article de Colin Damon, et je vous conseillerai d’être témoin de la réalisation concrète avec le replay du live d’Hippolyte Durix et Colin Damon).

Une fois qu’on adhère à sa structure, il devient aisé de faire vivre la base de code. Il ne reste plus qu’à se réconcilier avec l’outillage de test ! C’est probablement la partie qui m’a fait vraiment “mal”, en utilisant le Test-Driven Development (TDD) afin de remettre le comportement désiré au centre du développement de sa solution. Ainsi, il ne peut pas y avoir la moindre ligne de code écrite sans test. Au-delà d’une technique de développement, cette technique de conception permet de poser la question explicitement : “Qu’est-ce que tu cherches à vérifier ?” Lorsqu’on répond à cette question, en développant le test automatisé qui matérialise la réponse, alors l’implémentation du morceau de fonctionnalité découle naturellement. J’ai eu mal, très très mal, pendant un mois complet, pour deux raisons :

  1. Mon cerveau a travaillé pendant 10 ans dans l’autre sens. Il était “naturel” pour moi de localiser l’endroit où je souhaitais ajouter la modification, et… de l’ajouter. Directement. Déconstruire ma façon de travailler a probablement été le plus horrible. Un syndrome de la page blanche quasi garanti les premiers jours. Avis des experts pour y aller en douceur : commencer par découvrir le TDD sur des katas, plutôt que sur un vrai projet complexe (voir l’article de Colin sur l’apprentissage du TDD).
  2. Ma “peur” des frameworks de test (hormis JUnit) est revenue de plus belle et en première ligne. Impossible de passer à côté, pas d’esquive, de “pas le temps”, “une autre fois”, “je vais tester à la main ça ira plus vite”… On met les mains dans l’plat, les pieds dans l’cambouis… ou inversement, enfin vous m’avez compris… et me faites pas répéter.

Pour autant, je ne reviendrai pour rien au monde en arrière. Je pleure en pensant qu’il m’a fallu 10 ans pour découvrir ceci !

Pas besoin d’aller plus loin : quand l’architecture de la solution est maintenable, et que l’on fait du TDD, on commence forcément à avoir confiance en sa base de tests.

La vie n’est pas entièrement rose pour autant, mais sur une échelle de “une semaine de tests manuels” à “livraison continue en production”, lorsque les principes précédents sont appliqués, il reste très peu de chemin à parcourir pour appliquer le déploiement continu. Ce qui pourrait très certainement être apporté par le Behavior-Driven Development (BDD) qui remet l’alignement de la compréhension métier au cœur des préoccupations, et je vous conseillerai l’article de Grégory El Haimer. Effectivement, les quelques problèmes que je constate au quotidien sont soit un “trou” entre plusieurs bounded context (la propagation d’un événement qui est mal vérifiée), soit une incompréhension du besoin métier qui aurait pu être évitée avec du BDD. L’équipe de développement a beau mettre du cœur à l’ouvrage, si la compréhension fine d’une fonctionnalité est différente entre chaque personne, le résultat produit ne sera tristement pas le bon.

Vous me direz que j’ai laissé de côté les problèmes 2 et 4… Ils sont marginaux une fois qu’on a répondu aux deux autres. Lorsque l’équipe de développement a prouvé qu’elle avait elle-même confiance en sa qualité, avoir une équipe de tests devant tester manuellement pour “rattraper les erreurs” de l’équipe de développement devient un non-sens organisationnel.

Concernant la justification du temps, lorsqu’on développe en TDD, il n’est même pas possible d’imaginer sortir “la charge de tests”. De plus, la charge de tests se résorbe naturellement puisque la conception elle-même émerge des tests, et n’est au final plus une charge, mais un réel gain d’efficacité. Finalement, il convient de voir l’image dans son intégralité : un bug aperçu trop tard est une véritable perte de temps distribuée. 20 minutes de dév’ supplémentaires pourraient éviter que X utilisateurs rencontrent le problème et s’énervent chacun dans leur coin, avant qu’enfin un utilisateur remonte le problème, que le Product Owner refasse un test pour comprendre l’erreur, le priorise, l’amène à nouveau au développeur, qui va corriger et demander à ses collègues une relecture de merge request... Un bug va obligatoirement engendrer plusieurs heures de perte pour l’organisation au global, en plus de créer des frustrations, de la perte d’image de marque, de détérioration de la confiance envers le produit, des mauvaises notes sur les stores et un risque de perte définitive du client.

Et dans l’idée osée où nous serions extrêmement confiants de notre qualité sans tests automatisés, quid du reste de l’équipe, de l’attractivité des pratiques de développement pour le recrutement, et du transfert de connaissances ? Lorsque les cas métier sont explicitement testés, n’importe quelle personne fraîchement intégrée aura confiance en sa base de code et prendra plaisir à contribuer.

Un socle de tests pertinent, permettant d’afficher explicitement la bonne compréhension métier du produit, est la véritable clé vers un déploiement continu en production, et vers une réactivité de livraison de fonctionnalités. Alors n’hésitez plus, testez !