Publié par
Il y a 5 mois · 19 minutes · Agile

Agile smells – Pratiques de développement et de test

Suite de la série d’articles consacrée aux Agile smells, je vous propose, au travers de situations réellement vécues, de faire un tour d’horizon des dérives, des fausses bonnes idées ou simplement des phrases prononcées qui peuvent vous amener à vous dire que quelque chose sent mauvais. Aujourd’hui, parlons des pratiques de développement et de test…

Si vous avez manqué un des épisodes de la série des Agile smells, vous pouvez les retrouver ici :

Une user story par développeur

La situation

Au moment d’entamer une nouvelle itération, chaque développeur entreprend l’implémentation de sa propre user story. Au final, pour quatre développeurs, il y a quatre user stories en cours de développement.

Pourquoi ça sent mauvais ?

L’argument principal généralement mis en avant pour justifier cette manière de travailler se résume au fait d’obtenir une meilleure productivité. Selon le contexte, cet argumentation peut parfois avoir du sens mais seulement sur une vision très court-termiste (au maximum l’itération en cours). En revanche cette pratique engendre des dégâts bien plus concrets que cet hypothétique gain de productivité :

  • Un code hétérogène

Il existe autant de manières de coder que de développeurs présents dans l’équipe. Bien que des standards soient généralement décidés, chacun à sa manière de penser son code, d’utiliser tel ou tel pattern dans un contexte précis, d’écrire des tests ou d’améliorer l’existant de manière automatique. Le niveau de maintenabilité d’une telle base de code devient alors rapidement complexe, engendrant des problèmes de qualité de l’application et des temps de développement en augmentation. Une productivité en baisse donc.

  • Une connaissance mal partagée

Étant donné qu’un seul développeur est responsable d’une user story, la connaissance engendrée lors de l’implémentation n’est donc bénéfique qu’à une seule personne. La dérive souvent constatée est alors de voir apparaître une spécialisation des personnes : le développeur qui s’est occupé d’une partie de l’application va être de nouveau sollicité quelques itérations plus tard lorsqu’une nouvelle user story ou un bug concernant cette partie seront présentés. Un départ ou simplement des absences seront alors vécus difficilement par les équipiers qui devront reprendre un code qui « n’est pas le leur » et qui leur demande un effort de montée en compétence dans l’urgence.

  • Des individualités, pas une équipe

Au final, chaque personne n’étant intéressée que par l’accomplissement de sa tâche, il n’est nullement question de travail d’équipe. Le code devient la propriété d’un développeur et non plus de l’équipe ce qui aura pour conséquence de désigner systématiquement un responsable lors de la survenance de problème majeur.

Concrètement on fait quoi ?

Face à ce genre de situation, une première action simple à mettre en place est la revue de code. Elle doit permettre de :

  • Partager la connaissance sur ce qui a été réalisé
  • Vérifier le fonctionnement du code
  • Relire le code pour s’assurer qu’il correspond aux standards
  • Aligner les pratiques au sens de l’équipe

Bien que faisant dorénavant partie des standards dans le processus de développement, elle a tout de même certaines limites. La personne effectuant la revue de code est en position passive dans le sens où elle va regarder un code existant en se posant juste la question « ce code est-il bon ? », ce qui dans de nombreux cas équivaudra simplement à vérifier que le code respecte les standards. De plus, cette boucle de feedback est parfois trop longue, notamment lorsqu’on attend la fin d’un développement de plusieurs jours pour le remettre en question alors qu’il aurait pu l’être dès le premier jour. Enfin elle ne résout pas entièrement le problème de travail en équipe.

Si l’on se réfère aux pratiques liées à l’Extreme Programming (XP), on ne retrouve pas la revue de code effectuée à posteriori. Cette méthodologie agile préconise que tout développement soit effectué par deux développeurs travaillant ensemble : le pair programming. Le code est alors revu en temps réel de manière active, ce qui garanti qu’à tout moment il y a plus d’une personne qui a la connaissance de cette partie de l’application. Extreme Programming promeut également l’appropriation collective du code (Code Collective Ownership), qui de part la pratique du pair programming permet à des parties de l’application souvent impactées d’être parcourues par de nombreux développeurs, et met en avant l’amélioration de manière systématique d’un code modifié.

Le résultat est alors des développeurs qui s’alignent tous sur les mêmes pratiques de développement, qui sont responsables ensemble de la moindre ligne de code et qui sont capables de manière autonome de répondre à n’importe quelle problématique sur toutes les parties de l’application.

Pour une explication plus détaillée du pair programming et des pièges à éviter dans sa pratique, vous pouvez lire cet article dédié qui, bien que datant un peu, répond parfaitement au sujet.

En résumé, il est préférable de privilégier au maximum le pair programming et, à défaut de pouvoir le faire, réaliser des revues de code. Enfin pour les plus courageux, le Mob programming sera l’étape ultime !

« Non j’ai pas lancé les tests en local car le temps qu’ils s’exécutent quelqu’un va commiter et je me taperai un merge de la mort encore… »

La situation

Sur un projet réunissant vingt-cinq développeurs répartis en quatre feature teams et travaillant sur une base de code commune, les tests de l’application s’exécutent en un peu plus de trente minutes. Du fait des ces temps d’exécution, les développeurs sont souvent amenés à pousser régulièrement leur code directement vers le dépôt commun sans forcément lancer les tests avant par peur des merges fréquents et peu appréciés.

Pourquoi ça sent mauvais ?

Cette situation met en avant deux problématiques :

  • Un temps d’exécution des tests trop important

Il est difficile pour un développeur, lorsqu’il a terminé son développement, de lancer l’ensemble des tests de non régression sur son poste de développement si ces tests sont très longs à s’exécuter. Perdre une heure par jour à attendre l’exécution de tests n’est effectivement pas concevable même si le but initial est pourtant noble : s’assurer qu’on ne va pas faire passer le build de l’intégration continue au rouge, empêchant ainsi l’ensemble des développeurs de partager leur code à leur tour.

En début de projet, les équipes ont souvent tendance à privilégier des tests de haut niveau (test d’IHM par exemple) qui sont souvent faciles à mettre en œuvre et compréhensibles par des personnes non techniques. Le problème est que ce type de test est plus long à s’exécuter, et ce qui n’était pas gênant au début du projet, devient avec le temps très chronophage. De plus, ces tests sont plus sensibles à la moindre modification de fonctionnalité, nécessitant un coût de maintenance non négligeable. Enfin, plus le nombre de ces tests est important, moins leur couverture est maîtrisée. Il n’est alors pas rare de trouver de nombreux tests qui couvrent pour 90% le même périmètre.

  • Des merges fréquents et complexes

Lorsque les développeurs travaillent tous sur un dépôt commun, les risques de merge sont forcément plus importants. De plus, attendre d’avoir tout développer avant de partager son code, une sorte de « gros commit » embarquant plusieurs jours de travail, est un risque supplémentaire de devoir faire face à un merge plus difficile.

Enfin, lorsque des tests sont en échec, toutes les personnes qui souhaiteraient partager leur code sont bloquées. Un build souvent KO aura tendance à générer des files d’attente et le retour au vert sera synonyme d’anarchie, tous les développeurs essayant de partager leur code en même temps. Ces mécanismes auront pour conséquence de créer de la frustration liée à l’impossibilité chronique de partager son code, et au final un énervement aboutissant à de plus mauvaises pratiques (partage sur build KO par exemple).

En résumé, dans cette situation, entre l’exécution des tests, la gestion des merges et l’attente d’un build vert, le temps perdu par les développeurs est très conséquent. Par un calcul simple, on s’est aperçu qu’il s’élevait à environ une journée par personne par itération.

Concrètement on fait quoi ?

Dans une telle situation, et avant de partir sur une chantier plus couteux, deux actions simples peuvent être rapidement mises en œuvre :

  • Catégoriser les tests : le but est de sélectionner les tests à exécuter en local (hors tests unitaires qui sont rapides et à exécuter systématiquement). Au travers de tags par exemple, sélectionner un sous ensemble de tests qui sont dit « fragiles », à savoir très réactifs à une quelconque modification de code. Seuls ces tests seront joués en local, les autres seront déroulés par la plateforme d’intégration continue suite au partage du code. Cela ne règle pas le problème mais permet de réduire les risques avec un temps d’exécution minimum.
  • Travailler par branche : des outils comme Git ont simplifié l’utilisation des branches de développement, permettant ainsi de déporter l’exécution des tests de la branche dédiée sur la plateforme d’intégration continue. Cela permet aux développeurs de ne pas être bloqués par le temps d’exécution des tests en local et de ne pas déranger l’ensemble des développeurs dans le cas où un test échouerait. Cela ne réduit en rien le coût des merges mais a le mérite de garantir la branche commune toujours saine.

À présent que l’hémorragie a été ralentie, un chantier plus complexe est à mener sur les tests. De manière à éviter des temps d’exécution rapidement insupportables, une stratégie d’automatisation des tests doit être mise en place. Dans son livre Succeding with Agile, Mike Cohn introduit le concept de pyramide des tests suivant :

Illustration Martin Fowler

Cette pyramide définit trois principales catégories de tests à automatiser :

  • Tests unitaires : représentent la grande majorité des tests à automatiser. À la base des pratiques de développement comme le TDD, ils offrent un feedback immédiat du fait de leur rapide exécution, et une précision dans leur résultat grâce à leur faible périmètre.
  • Tests fonctionnels : souvent associés à des tests d’intégration, ils peuvent permettre de tester des API ou de simuler des parcours utilisateurs sans la partie IHM. En d’autres termes, s’assurer que le produit répond bien aux besoins utilisateurs. Des pratiques comme l’ATDD ou spécification par l’exemple sont généralement associées à ce type de test (bien qu’une partie puisse être également écrite en tests unitaires) avec une attention tout de même à ne pas automatiser l’ensemble des tests d’acceptation mais seulement un sous ensemble, de manière à éviter des temps d’exécution trop importants.
  • Tests d’interface utilisateur : simulent les interactions avec un utilisateur. Très lents à s’exécuter et difficiles à maintenir, ils ne représentent qu’une faible proportion des tests afin de valider quelques parcours de bout en bout (généralement les parcours principaux qui touchent une majorité des utilisateurs) sur des navigateurs différents.

Sur cette base, reprenons la situation telle que décrite précédemment : les tests des couches supérieures de la pyramide sont trop nombreux entrainant un temps d’exécution important. Que faire alors pour résoudre ce problème ? Revoir l’ensemble des tests dans un chantier immense de refactor n’est pas une solution car cela prendrait trop de temps et empêcherait les équipes de délivrer de la valeur. Ajoutons que ce genre de chantier n’est absolument pas motivant pour les développeurs. Dans une telle situation, il faut savoir profiter des user stories comme des opportunités pour remettre en question les tests qu’elles font échouer, de la manière suivante :

  • Le test est-il toujours d’actualité ? Peut-il être supprimé ou fusionné avec un autre dont le périmètre est proche ?
  • S’il s’agit d’un test IHM, y a-t-il une raison valable de ne pas le réécrire en test de fonctionnalité ?

Au-delà des tests automatisés, une stratégie de test plus globale est à considérer. Lisa Crispin et Brian Marick ont introduit les quadrants suivants :

Suivant une vision technique ou métier, et en fonction des intentions d’aide ou de remise en question, ils ont divisé les tests en quatre catégories :

  • Q1 : ils ont pour but d’aider l’équipe dans l’amélioration du design de code et dans l’atteinte d’une qualité de code optimale. Associés à des pratiques comme le TDD, ils sont systématiquement automatisés.
  • Q2 : ils permettent de diriger les développements sur les comportements métiers. Associés à des pratiques comme l’ATDD ou la spécification par l’exemple, seul un sous ensemble est automatisé. Retrouvez la série d’articles À la découverte de l’ATDD pour une introduction ludique à cette méthode de travail.
  • Q3 : ils consistent en l’évaluation du produit. Impliquant davantage de collaboration avec les utilisateurs réels, ces tests sont entièrement manuels.
  • Q4 : ils permettent de « stresser » l’application sur un point particulier (performance, sécurité, etc.). Ils sont généralement effectués au travers d’outils spécialisés et parfois automatisés.

« 28 todos et 12 fixme, mais tout va bien sinon ! »

La situation

Sur la base de code d’une application d’un an et demi d’existence, on peut compter en tout quarante annotations « TODO » et « FIXME ».

Pourquoi ça sent mauvais ?

Avant toute chose, il est nécessaire de bien distinguer les termes :

  • FIXME : indique qu’un bout de code a un problème et qu’il peut potentiellement être source de bug. Autrement dit, qu’il peut avoir un impact important sur l’utilisateur final.
  • TODO : indique qu’un bout de code qui fonctionne en l’état, aurait besoin d’une amélioration significative. Autrement dit, qu’il peut avoir un impact important sur la complexité d’un développement futur.

Dans cette situation, en faisant un calcul simple sur la base d’itération de deux semaines, on compte donc l’ajout d’une annotation de ce type chaque itération. En d’autre terme, cela signifie que chaque itération abouti à la création d’un bout de code dont la qualité est à revoir et dont l’impact est potentiellement dangereux.

L’origine de cette situation est généralement toujours la même : l’équipe de développement est continuellement mise sous pression pour sortir le plus rapidement possible un nombre de user stories important afin de respecter des délais toujours plus courts.

Les conséquences sont alors :

  • Un nombre de bugs qui augmente significativement : la charge de travail de l’équipe va alors être encore plus importante.
  • Une maintenance du code existant toujours plus difficile : l’ajout ou l’évolution d’une fonctionnalité deviendront de plus en plus couteux en terme de complexité.
  • La motivation de l’équipe en baisse : travailler sur du code de mauvaise qualité peut assez rapidement devenir une souffrance et avoir un impact non négligeable sur la motivation.
  • Le recours par l’équipe à une surestimation des user stories pour se donner une marge « anti stress » : ce qui aura pour conséquence de briser le lien de confiance censé exister entre les développeurs et le Product Owner.

Concrètement on fait quoi ?

La première chose à faire est d’officialiser la présence de ce qu’on appelle généralement la dette technique. Se servir du management visuel est une bonne pratique afin de mettre en évidence les problèmes quotidiens de l’équipe. En affichant clairement les quarante post-it sur un mur, l’impact que cela peut avoir sur les personnes peut être significatif. Attention tout de même, un amas de post-it disposés n’importe comment peut avoir l’effet inverse de celui escompté, et donner la sensation qu’il y a trop de choses à faire, provocant ainsi une certaine forme de procrastination et des post-it qui finiront par ne plus coller du fait de leur vieil âge.

Un bon moyen d’éviter cela est de faire le même exercice que pour les user stories d’un backlog : pour chaque item de la dette technique, estimez le risque pour l’application et la complexité de réalisation. Créez alors des catégories sur votre tableau dans lesquelles les post-it seront triés. Vous pourrez avoir par exemple les quatre catégories suivantes en partant de la plus prioritaire :

  1. plus risqué / moins complexe
  2. moins risqué / moins complexe
  3. plus risqué / plus complexe
  4. moins risqué / plus complexe

L’idée de se focaliser en priorité sur les items les moins complexes est de faire baisser rapidement le nombre total d’items restant. Cela aura un impact positif sur l’équipe et les motivera davantage à continuer les efforts. Il restera des items plus risqués non traités mais en considérant qu’ils étaient déjà là depuis plusieurs mois, quelques semaines supplémentaires ne feront pas plus de mal.

C’est sur cette base que peut alors se dérouler la négociation avec le Product Owner. Avec cette vision claire et transparente des difficultés quotidiennes des développeurs, et avec, si besoin, des métriques sur l’impact direct de la dette sur la complexité des user stories, l’équipe trouvera un consensus sur le meilleur moyen de traiter la dette. Quelques exemples concrets de solutions qui ont permis de réduire la dette :

  • Une demi-journée voire une journée entière entre deux itérations entièrement dédiée à la résolution des items de la dette technique.
  • Un ou deux post-it de la dette technique intégrés au Sprint Backlog à chaque itération.
  • Une personne exclusivement dédiée à la résolution de la dette à chaque itération sur la base d’un rôle tournant.

Toutes ces stratégies sont évidemment préconisées dans le contexte précis de notre situation : la dette technique vient d’être identifiée et il est nécessaire de fournir un effort important pour rapidement la diminuer. Dans un même temps, il est nécessaire de ne pas engendrer de nouvelle dette pendant le développement des user stories. C’est ici que les techniques de développement (pair programming, revue de code, …) et de test (TDD, BDD, …) prennent encore une fois tout leur sens.

Pour approfondir le sujet, vous pouvez vous tourner vers le livre blanc dédié à la dette technique ou plus largement le TechTrends Craftsmanship.

« On a mis en place une généricité de fou, l’ajout de règle sera super simple maintenant ! »

La situation

Dans le cadre d’un produit à destination d’entreprise et de particulier, une équipe doit prendre en compte une évolution sur la partie facturation. Une première user story est prise dans l’itération et concerne la partie entreprise. Une deuxième user story concernant les particuliers sera elle prévue quelques itérations plus tard car moins urgente et pas totalement définie.

L’équipe décide de prendre du temps supplémentaire pour implémenter une solution générique qui non seulement prendra en compte les règles pour les entreprises, mais permettra également de faciliter l’intégration des règles pour les particuliers.

Pourquoi ça sent mauvais ?

Plusieurs coûts sont engendrés par ce genre de pratique :

  • Coût de construction

Le problème avec un besoin prévu dans plusieurs mois est qu’il reste hypothétique, ce qui signifie qu’il peut évoluer voire disparaître. Tous les efforts passés à analyser, développer et tester n’auront alors servi à rien.

  • Coût de délai

La prise en compte d’un besoin futur dans la conception (ici la mise en place de la généricité) nécessite du temps supplémentaire par rapport à l’implémentation d’une solution simple. Ce temps n’a alors pas permis à l’équipe de développer d’autres fonctionnalités concrètes et livrables immédiatement, apportant ainsi davantage de valeur au produit. Dans notre situation, un délai supplémentaire sera nécessaire pour l’atteinte du même niveau de valeur.

  • Coût de complexité

Implémenter des besoins futurs a souvent tendance à ajouter de la complexité au code. Ce dernier devient alors plus difficile à maintenir et l’ajout de nouvelles fonctionnalités aura un surcoût.

Concrètement on fait quoi ?

Pour ce genre de situation, un principe de l’Extreme Programming est à suivre : le Simple Design. Souvent associé au mantra YAGNI (You Aren’t Gonna Need It), il présume que toutes les fonctionnalités dont le produit aura besoin dans le futur ne doivent pas être construites maintenant tout simplement car vous n’en avez pas besoin.

Dans notre situation, cela aurait alors impliqué de réaliser de la plus simple des manières la partie facturation des entreprises, répondant ainsi au besoin rapidement. Puis quelques mois plus tard, lors de la prise en compte du besoin pour les particuliers (qui cette fois-ci est totalement maitrisé), une réflexion sur le meilleur moyen de s’intégrer dans l’existant aurait été menée, aboutissant à la mise en place de la généricité ou non. Le recul et l’expérience accumulée ayant une influence non négligeable sur la conception, la solution de généricité n’aurait peut être pas finalement été choisie.

Vous pouvez retrouver un article plus complet de Martin Fowler sur le YAGNI reprenant les principaux éléments mis en avant ici.

Conclusion

Au travers de quatre situations, nous avons découvert comment les pratiques de développement et de test pouvaient se révéler être contre productives, mais également, comment elles pouvaient être une aide d’une grande efficacité pour améliorer de nombreux aspects de la vie d’une équipe. Ne jetez pas votre désodorisant tout de suite, nous revenons bientôt avec de nouveaux agile smells dédiés cette fois-ci aux rôles et à la motivation.

Julien Rossignol
Studio Team Leader

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *