Le piège des tests unitaires

Voilà maintenant plus de 5 ans que j’applique une approche TDD (test-driven development) sur l’ensemble des projets sur lesquels j’interviens. Si j’utilise toujours cette méthode, c’est parce que la présence de tests me donne confiance dans le code que j’écris : 

  • Je m’assure qu’il fait bien ce que je souhaite.
  • J’améliore constamment son design par du refactoring.
  • Les tests mettent en lumière la très grande majorité des régressions que je peux introduire lors d’un refactoring ou d’une évolution.
  • Je réduis ma charge cognitive et me focalise sur le cas métier que je suis entrain de traiter (les tests s’occupent de vérifier les autres cas pour moi).

Modifier mon code est donc une capacité permise grâce aux tests et que je souhaite conserver tout au long d’un projet : je peux améliorer son design pour le garder constamment adapté aux problèmes métier que je veux résoudre.

Avec le temps, je réalise qu’écrire des tests est une discipline difficile et que certaines pratiques peuvent être dommageables. Les tests unitaires “figent” le code et m’empêchent de le modifier facilement, ce qui peut sembler paradoxal puisqu’ils sont censés au contraire me le permettre.

Dans cet article, je vais essayer de mettre en lumière les raisons de cette dérive. Attention, mon propos ne sera pas focalisé sur la pratique du TDD mais sur les tests unitaires (TU) et la manière dont ils influencent notre capacité à modifier du code.

Écris une classe, écris une classe de tests

À chaque classe, chaque méthode son ou ses tests : C’est une définition du TU qui semble très répandue, je la rencontre beaucoup au cours de discussions avec d’autres développeurs, sur twitter, dans des articles de blog, etc. C’est même de cette manière que l’on m’a initié au TDD.

Il est vrai que c’est une façon simple d’écrire un test. Le périmètre que l’on souhaite tester est petit, avec un nombre de dépendances normalement raisonnable. C’est une approche vers laquelle on peut facilement se tourner lorsque l’on n’est pas à l’aise avec la rédaction de TU.

Cette pratique est souvent associée à l’injection de dépendance pour fonctionner. C’est à ce moment-là que l’on commence à introduire des mocks, on peut ainsi spécifier le comportement d’une dépendance sans dépendre de sa véritable implémentation. Notre classe/méthode reste donc bien isolée du reste du système lors du test. 

Mon point doit peut-être vous déranger : pourquoi vouloir absolument tester en isolement une classe qui de toute façon dépend d’autres classes ? Le comportement de la dépendance correspondra-t-il à celui que j’ai spécifié avec mon mock ?

Certains vous répondront qu’il vous faut également des tests d’intégrations. Afin de vérifier que les différents éléments du système interagissent de la bonne manière, et que le logiciel retourne le résultat attendu. 

Vous avez déjà perdu

Si vous adoptez cette stratégie, vous allez surement souffrir, ou souffrez déjà, d’une forte adhérence entre votre code et vos tests. En effet, il existe une contrainte forte pour chaque élément de votre système : des tests.

Cela signifie qu’à chaque signature de méthode que l’on veut changer, en plus du code l’appelant, il va falloir corriger les tests et les mocks qui lui sont associés. Nous avons donc perdu notre capacité à facilement modifier/refactorer notre code. J’ai encore le souvenir (douloureux) de journées complètes à “réparer les tests” suite à des modifications.

Ajoutez à cela un biais dont nous souffrons tous : celui des coûts irrécupérables. Alors qu’un test ne fait peut-être plus sens, nous avons tendance (consciemment ou non) à vouloir le conserver et le modifier, ceci uniquement parce qu’il est déjà écrit.

Vous l’avez compris, aligner de manière aussi systématique ses tests avec son implémentation génère un couplage important contre lequel vous luttez à chaque modification que vous souhaitez apporter.

Du coup, comment fait-on ?

Les TU impliquent obligatoirement un couplage avec le code. Même s’il existe des techniques pour le limiter, la première question qu’il faut se poser est : à quoi veut-on se coupler ?

De manière caricaturale, si vous travaillez sur des logiciels business, ce que l’on attend de vous est de développer des use cases. Ceux-ci sont des comportements que l’on attend de l’application, et il fait sens de vouloir les tester. Ces use cases peuvent être implémentés en une seule classe, ou en plusieurs. Ce sont des choix de design qui vous reviennent, mais aussi des détails d’implémentation que l’on veut pouvoir facilement changer et qu’un observateur externe du système doit ignorer.

Clairement, la stratégie que l’on vient d’explorer s’attache beaucoup à ces détails d’implémentation. Unitaire ne doit donc pas définir la taille de la portion de code que l’on veut tester. 

Je vous propose maintenant la définition que j’ai en tête quand je parle de TU: un test que l’on peut exécuter en isolation et dont le comportement est répétable et constant. 

Son résultat ne dépend donc pas du résultat d’autres tests ni de l’état de dépendances externes au système (appel à une web API, à une base de données). Notez que je ne définis pas la taille du périmètre testé.

Des tests de comportement

Aujourd’hui, j’adopte autant que possible une stratégie qui me permet de conserver ma capacité à modifier le code.

J’écris mes tests de sorte qu’ils dépendent uniquement des contrats entre mon système et le monde extérieur (endpoint REST, base de données, bus de données, etc.). Tout le reste est une boîte noire inaccessible. 

Ainsi, un test se présente typiquement de la façon suivante : 

  1. Je définis l’état du système (ex: données en BDD)
  2. Je lance une action métier via une API publique (ex: endpoint REST)
  3. Je vérifie le nouvel état de mon système (ex: données en BDD) et les éventuelles interactions avec le monde extérieur (ex: publication d’un message sur un bus)

test-strat1

Pour écrire ces tests et les garder indépendants, j’utilise des implémentations “in memory” de mes dépendances externes (typiquement la base de données). Je m’assure également que ces implémentations ont des comportements identiques à celles que j’utilise réellement en production. Pour cela, j’écris quelques tests d’intégrations paramétrés qui testent uniquement les accès au monde extérieur.

test-strat2

Cette approche peut paraître extrême, mais elle répond au problème que je souhaite adresser dans cet article.

Je dois tout de même lui reconnaître certaines faiblesses : 

  • Des boucles de feedbacks plus lentes.
  • Des erreurs parfois plus dures à analyser.
  • Il reste un couplage à certaines couches applicatives.

Créer des abstractions

Il est tout de même nécessaire d’aller un peu plus loin pour réduire l’adhérence avec les tests. En effet, si nous avons réduit aux interfaces publiques la surface à laquelle nous sommes couplés, celles-ci deviennent encore plus critiques en cas de modification. 

Par expérience, un bon moyen de mesurer le couplage à un contrat est de compter le nombre d’endroits où celui-ci est instancié.

builders

Builders, générateurs ou encore données statiques, quel que soit la ou les technique(s) utilisée(s), l’objectif reste toujours le même : isoler la création d’une donnée, d’un service, de l’application, de sorte que si sa structure change, il ne faille appliquer ce changement qu’à un seul endroit dans les tests. 

Un autre bénéfice de ces méthodes est qu’elles rendent les tests plus clairs, puisque vous n’avez à spécifier que les données qui font sens pour le scénario. Avec le temps, vous arriverez sans doute à faire émerger un DSL pour vos tests, l’ajout de nouveaux use cases en sera facilité voir presque trivial.

test-strat3

Pour conclure

Il m’est arrivé plusieurs fois de rencontrer des gens qui ont tenté de mettre en place des tests unitaires ou du TDD sur leur projet, et qui y ont finalement renoncé après quelque temps parce que “ça ne marche pas”. Je pense que le problème que j’ai évoqué au cours de cet article est la principale raison de ces abandons.

Attention également aux dogmes ! Si je me montre critique envers les tests unitaires “petite maille”, ceux-ci peuvent se révéler utiles et parfois plus simples qu’un test “boîte noire”. De la même manière, il est parfois plus simple d’utiliser un mock plutôt que de définir l’état du système dans sa globalité. Il est ici question de compromis, de choix qui doivent être faits en connaissance de cause.

Si cet article vous a plu, je vous recommande de regarder la conférence DevTernity 2017: Ian Cooper – TDD, Where Did It All Go Wrong.

 

Edit 1 : Je vous partage également cet article que l’on m’a montré en réaction à ce post et qui explique sans doute mieux que moi le point que je souhaitais traiter ici.

Edit 2 : Deux autres remarques m’ont été faites :

  • Certains tests sont déjà naturellement isolés (domaine, hexagone) et ne dépendent pas de détails d’implémentations. Ceux-ci sont aussi viables que la stratégie que je propose dans cet article.
  • Si les tests « petite maille » ne sont pas idéals sur le long terme, ils peuvent être très utiles comme « échaffaudage » pour implémenter progressivement un scénario plus vaste. À condition de les supprimer une fois les conditions d’un test « grosse maille » remplies.
Le piège des tests unitaires

Un code métier pur

Il y a quelques jours, au cours d’une discussion, on m’a demandé quelles sont les pratiques que je pousse dans une équipe dans le but d’améliorer la qualité de code. Bon nombre de pratiques comme TDD, clean code ou encore DDD et ses bounded-contexts ayant déjà été cités, j’ai donc répondu : un code métier pur, parfois appelé functional core.

Dans cet article, je pars du principe que vous faite une distinction et séparation forte entre le code métier qui répond à une logique business, et le code infra qui répond aux problématiques techniques.

Quels intérêts ?

Un code que l’on peut qualifier de pur a deux caractéristiques : 

  • Celui-ci retourne toujours le même résultat pour les mêmes entrées. Il ne dépend donc d’aucun état interne ni d’appels à des dépendances (base de données, heure système, etc.)
  • Il ne modifie aucun état visible du système.

Les raisons pour lesquelles je pousse ce genre de pratiques sont extrêmement simples. Il m’est très facile de raisonner sur ce code puisque son comportement est à la fois prédictible et répétable. 

Il est également très simple de rédiger des tests pour ce genre de code. Vous pouvez donc décrire tous vos cas métiers sous cette forme : “mon système est dans cet état, je lance cette action, alors j’obtiens ce résultat”.

Par exemple, un scénario pour la réservation d’un parking : 

  • J’ai renseigné mes dates et heures d’arrivée et de départ .
  • Je valide ma réservation.
  • Ma réservation est acceptée pour les dates .

Si l’on peut parfois considérer les problèmes de charge comme inhérents au métier, on a tout de même envie de les traiter comme des problématiques techniques. Gérer l’accès à un état partagé sur lequel on souhaite écrire se révèle vite complexe (usage de lock, de transactions par exemple) et empêche un code scaler. Nous ne voulons donc pas polluer de la logique métier avec ce genre de problématiques : garder le code pur est une façon simple de s’en assurer.

La raison d’être d’un logiciel 

Cependant, si nous écrivons des logiciels, c’est souvent pour produire ce que nous qualifions jusqu’à maintenant d’effet de bords : écrire en base de données, envoyer un mail, une notification, etc. Nous devons donc être capable de passer d’un code pur à impure et inversement. 

Une façon (peut-être simpliste) de voir un logiciel est une succession de transformations de données. Je veux lire une donnée sur mon disque dur (imprédictible), puis la transformer (prédictible) et enfin écrire le résultat sur mon disque (imprédictible). 

Comment faire vivre les deux ?

Nous avons vu jusqu’ici qu’il doit y avoir une distinction claire entre, le code métier que l’on veut pure, et le code infra qui lui est nécessairement impure puisque sa responsabilité est de traiter avec des appels réseaux et système. 

Pour faire cohabiter ces deux mondes, il nous faut donc un bout de code dont la seule responsabilité est : 

  1. De récupérer les données nécessaires à une opération métier.
  2. Appeler le code métier.
  3. Envoyer le résultat à la couche d’infrastructure.

Répondre à cette problématique de séparation métier/infra est la principale motivation derrière l’architecture hexagonale. Dans cette architecture, nos services portent cette responsabilité :

pure1

Avec une architecture CQRS ou CQRS/ES, ce rôle est porté par le commandHandler.

pure2

Notez que la structure du code reste inchangée, seuls les types changent.

Ce pattern demande une certaine rigueur de la part des développeurs, il est en effet facile d’introduire des effets de bords dans le code métier. Pour cette raison la stratégie adoptée par Haskell consiste à encapsuler les effets de bord dans des IO monade. 

Je ne vais pas m’aventurer ici à définir ce qu’est une monade, mais si vous n’êtes pas familier avec ce concept, voici une image très grossière : Une monade est comme une boîte contenant de la donnée, pour manipuler cette donnée, vous devez fournir à la monade la fonction à appliquer. Une liste est par exemple une monade, l’IO monade en Haskell représente un effet de bord.

Dans cet exemple, j’ouvre le fichier input.txt, j’applique la fonction toUpperString puis j’écris le résultat dans le fichier output.txt. J’ai fait l’effort ici de décomposer les fonctions afin de voir les signatures. 

pure3

La transition du monde de l’IO vers du code pur se fait grâce à une fonction appelée fmap, ici appelée via l’opérateur <&>. fmap prend une fonction pure et l’applique un contenu d’une IO pour produire une nouvelle IO. On obtient ici un IO Uppercase.

Enfin, pour écrire le résultat, on applique la fonction writeOutput via la méthode bind (opérateur >>=). bind nous permet d’appliquer une fonction retournant une IO au contenu d’une IO.

Out of the Tar Pit

Si cet article vu a plu et que vous souhaitez approfondir le sujet, je vous encourage à lire le papier Out of the Tar Pit qui traite de la complexité logiciel, et qui propose un découpage similaire du code. J’ai découvert ce papier grâce à un talk explicatif de Romeu Moura.

Un code métier pur

À la découverte du property based testing

Je suis un développeur convaincu par les bénéfices du TDD, je l’applique au quotidien sur les projets que me confient mes clients. Cela me permet de rapidement valider que mon code a bien le comportement attendu, de le “documenter” et décrivant un cas d’usage et de m’assurer par la suite que je n’introduis aucune régression si je modifie le code testé.

Je fais tout ceci en sachant que je choisis des cas de test qui me semblent représentatifs de l’usage de la fonction, on parle parfois d’Example Based Tests. Si cette méthode est souvent suffisante, il m’arrive parfois de me poser les questions suivantes : Puis-je être sûr que ma fonction est correctement développée si je ne suis pas capable d’identifier un cas qui est représentatif ? Ai-je bien identifié tous les cas limites ?

C’est là qu’il devient intéressant de se pencher sur le property based testing.

C’est quoi le Property Based Testing (PBT) ?

L’idée est simple : identifier et tester des invariants. Comprenez quelque chose qui sera toujours vrai, quelles que soient les données que vous fournissez à votre algorithme.

Pour cela, il faut utiliser un framework qui va générer des données aléatoires et vérifier si l’invariant reste vrai. À chaque exécution de votre suite de tests, celui-ci va tester différentes combinaisons (généralement une centaine). Il est important de noter qu’un test de PBT en succès ne signifie pas que l’implémentation est correcte, il veut juste dire que le framework n’a pas su mettre en défaut l’implémentation. Il est tout à fait possible que celui-ci trouve un cas limite après plusieurs heures, jours, semaines, mois…

Ok, et si un test échoue ?

Si le framework arrive finalement à trouver un cas limite, il existe trois possibilités :

  • le code de production n’est pas correct
  • la façon dont l’invariant est testé n’est pas correcte
  • la compréhension et définition de l’invariant ne sont pas correctes

Il est important d’avoir cette réflexion dès qu’un cas est identifié. Quoi qu’il en soit, le framework est capable de vous donner les données utilisées pour mettre à mal votre code, vous pouvez donc facilement écrire un TU classique pour reproduire le cas.

Un bon framework de PBT est capable de faire du shrinking. Une fois le cas limite identifié, celui-ci va travailler sur les données utilisées pour essayer de les simplifier au maximum tout en reproduisant l’erreur. Ceci nous facilite l’effort d’analyse : imaginez une fonction qui prend une liste en argument, est-ce ma liste de 250 éléments ou juste un élément qui plante mon code ? S’il s’agit d’un élément, le shrinking peut l’isoler.

C’est quoi un invariant ? Un exemple ?

C’est à la fois tout l’intérêt de cette méthode de test, mais aussi toute sa difficulté. Il faut être capable de raisonner sur le métier de son application pour pouvoir en faire émerger des règles.

Un des premiers exemples que l’on peut rencontrer est celui de l’addition. Celle-ci a trois propriétés :

  • L’identité : x + 0 = x avec 0 comme élément neutre
  • L’associativité : (a + b) + c = a + (b + c)
  • La commutativité : a + b = b + a

Vous retrouvez également ces propriétés avec la multiplication, seul l’élément neutre change. Ces exemples sont très mathématiques (et peu intéressants), mais ce n’est pas le cas de toutes les propriétés, celles-ci peuvent prendre diverses formes.

Petite appartée, les exemples qui suivent sont écrits en F# avec FsCheck. J’ai volontairement  choisi un “mauvais” exemple métier puisqu’il est envisageable de tester tous les cas de manière unitaires, mais il me paraît très adapté pour illustrer ce qu’est un invariant.

Pour cet exemple, j’ai décidé d’écrire un petit programme qui doit me dire quelle est la main gagnante entre deux mains de deux cartes. Il s’agit d’une version simplifiée du Poker, si vous ne les connaissez pas, voici les règles :

  • si les deux mains sont équivalentes, alors il y a égalité
  • une paire gagne sur une main mixte (main avec deux cartes différentes)
  • s’il y a deux paires, la paire avec la meilleure carte gagne
  • s’il y a deux mains mixtes :
    • on compare la carte la plus forte de chaque main
    • si les cartes les plus fortes sont identiques, on compare les cartes les moins fortes
  • l’As est la meilleure carte et le deux la moins bonne

Je vous laisse quelques secondes pour trouver des invariants…

Vous avez trouvé ? Il s’agit tout simplement de la liste de règles que je viens d’énoncer : celles-ci sont toujours vraies.

Ok, prenons la première règle : “si deux mains sont équivalentes, alors il y a égalité”. Pour cela, je laisse le framework me générer deux cartes aléatoires (seule la valeur de la carte importe ici) qui constituent les deux mains :

draw

On peut aussi tester qu’une paire est toujours meilleure qu’une main mixte. Dans ce cas, on doit s’assurer que les cartes générées par le framework sont différentes. Pour cela, il est possible de poser des conditions qui, si elles sont respectées, permettent l’exécution du cas de test :

pair

Je ne vais pas détailler l’ensemble des cas de test sur cet article, vous pouvez tenter de le refaire de votre côté. Vous pourrez trouver une solution possible sur mon github.

Certaines imprécisions

Contrairement à un TU, il n’est pas toujours possible de spécifier le résultat exact que l’on attend à l’issue d’un test de PBT. Pour répondre à ceci, j’aime beaucoup la définition proposée par Jessica Kerr.

Un test de PBT défini un cadre métier dont on ne doit pas sortir :

L’idée est d’être moins spécifique sur le résultat, l’important est de s’assurer que les impératifs métiers sont validés. Cela présente l’avantage de laisser une plus grande liberté dans l’implémentation puisque son couplage avec le test est moins important.

Si l’on souhaite tout de même tester un résultat de manière exacte, alors il faudra revenir à un test unitaire avec un résultat hard-codé.

Pour conclure

On peut trouver certains inconvénients au PBT, comme des temps d’exécution un peu plus longs que des tests unitaires classiques, ou encore une plus grande difficulté à écrire ces tests.

Mais vous l’aurez compris, le PBT améliore notre compréhension de l’application puisqu’il pousse à raisonner à des niveaux d’abstraction plus élevés que ce que nous incite à faire des tests unitaires classiques : “une paire est meilleure qu’une main mixte” est à un niveau d’abstraction supérieur à “une paire de 5 est meilleure que la main avec le 8 et le roi”.

Enfin, parce que le code est validé par un grand nombre de cas différents, le PBT améliore également la qualité de notre code ainsi que la confiance que nous avons dans celui-ci.


Si après la lecture de cet article le PBT vous intéresse, vous pouvez également regarder ce talk de Jessica Kerr ou encore celui de Romeu Mourra qui sont pour moi des références sur ce sujet.

À la découverte du property based testing

Vous n’êtes pas maître de votre code

J’ai récemment pu participer à un atelier animé par Romeu Mourra lors des NCrafts. Pas de technique ici, le but était de mettre en lumière des problèmes d’ordres systémiques. Pour cela, nous avons fait un Kebab Kata sous forme d’itérations aux-cours desquelles Romeu jouait le rôle du client, puis également de l’architecte. Son but était de nous faire échouer en usant de différents comportements toxiques que l’on retrouve fréquemment dans de vraies missions.

Objectif rempli

L’atelier s’est déroulé de la façon suivante :

  • à chaque itération le client donne un périmètre et un budget (du temps) pour le réaliser.
  • pendant les itérations, le client répond aux sollicitations des équipes et va voir spontanément en tentant de les influencer.
  • à la fin de chaque itération, le client attend une démonstration.
  • pendant qu’une équipe réalise sa démonstration, les autres équipes ont le droit de “tricher” en continuant à coder.
  • une courte revue de code auto-organisée avec les autres équipes est mise en place après les démonstrations, il est interdit de coder durant cette période.
  • au bout de trois sprints, nous sommes “virés” puis recrutés en tant que nouvelle équipe. Il nous faut alors réaliser un audit et énoncer des actions à prendre sur le code.
  • un architecte, appuyé par le client, apporte alors des directives de conception en parallèle de notre audit.

Comme prévu, nous avons tous échoué : à l’issue de l’atelier, toutes les équipes ont considéré leur code comme étant du legacy.

Des responsabilités partagées

Il en ressort clairement que les développeurs ne sont pas les seuls responsables de la qualité finale du code. De façon synthétique et non exhaustive :

La contrainte la plus évidente est le temps sur un périmètre donné : les délais sont très courts et incitent à prendre des “raccourcis” comme ne pas utiliser de tests unitaires. Le client n’hésite pas à demander s’ils sont nécessaires. Il écoute les rares équipes qui tentent de négocier les délais mais cela n’aboutit à rien d’autre qu’à un moyen pour les développeurs d’exprimer leur frustration.

Les demandes du client ne sont pas claires ni priorisées : “si vous avez le temps, j’aimerais aussi cette feature”. Ce comportement ne fournit aucune visibilité à l’équipe, elle ne connaît pas la finalité du logiciel, ni le véritable besoin.

Tout comme le droit de “tricher” pendant les démonstrations, ce manque de visibilité incite les développeurs à constamment coder pour rattraper leur retard, ce qui a plusieurs effets pervers :

  • Aucun temps n’est alors accordé à la prise de recul, à la remise en question du code et de sa conception : l’équipe est constamment maintenue occupée au détriment de la qualité.
  • Le client n’a pas besoin d’exercer la moindre forme de management, les développeurs sont livrés à eux mêmes et subissent la situation.
  • L’équipe ne communique pas avec les autres pour échanger sur les solutions possibles. De plus, les temps accordés aux revues de codes sont inutiles car bien trop courts (et désorganisés) pour être constructifs : il n’est pas possible de faire émerger de réels axes d’amélioration

Enfin, les équipes subissent des pressions sur leurs choix techniques. Le client fait part des retours fait par l’équipe front end (dont l’existence n’avait d’ailleurs jamais été évoquée avant !) et des difficultés qu’elle rencontre lors de l’intégration. L’architecte impose, appuyé par le client qui “le paie très chère”, une architecture basée sur le design pattern composit. Il s’avère que cette solution répond bien au problème de conception de ce kata, mais ne reflète pas du tout la façon dont le métier du client peut évoluer, ce qui rend toute évolution encore plus coûteuse.

Prisonniers et gardiens d’un système

Avec tous ces éléments, les développeurs se sentent isolés puisque considérés comme de simples exécutants de décisions qu’ils ne comprennent pas et pour lesquelles ils n’ont pas été consultés. Il n’existe aucune confiance entre l’équipe de développement et ses interlocuteurs.

Romeu décrit le système dans lequel sont pris les développeurs comme étant un panoptique. La majeure partie des comportements observés lors de l’exercice peuvent être associés à trois piliers qu’il a identifiés :

  • le manque / l’absence de communication entre les équipes
  • la bonne visibilité du management sur les équipes
  • l’opacité du management pour les équipes

Une fois pris dans un tel système, les développeurs ont l’impression d’être constamment surveillés et ne se sentent plus libres de leurs manières de travailler. Ils s’imposent alors un mode de fonctionnement qu’ils finissent, à terme, par trouver normal. Malgré des lacunes plus ou moins évidentes de sa part, le système n’est alors plus remis en question.

C’est ainsi que ces mêmes développeurs peuvent se montrer hostiles à l’introduction de nouvelles pratiques comme le TDD ou le pair programming. Parce que cela ne leurs semble pas concevable et qu’ils craignent que le système rejette cela.

Tenter et innover

Bien que fréquents, les comportements évoqués plus haut ne sont pas adoptés pour sciemment nuire au projet. Il est tout de même important de savoir les identifier, les remettre en cause et initier des changements de méthode, de comportement.

Parmi les pratiques à mettre en place, Romeu proposait les suivantes :

  • Le mob programming pour rassembler les gens, les pousser à communiquer, comprendre ce qu’ils développent et pourquoi cela est nécessaire.
  • Supprimer la double contrainte temps / périmètre en appliquant notamment le no estimate. Un comportement qui peut être adopté serait de dire “Ok, je te livrerai uniquement ce qui sera prêt à cette date là” tout en ayant une vision claire des prioritées métier. Ce discours est parfaitement entendable contrairement à ce que l’on a tendance à penser.
  • Ne plus travailler à flux tendu : une équipe de développement est souvent perçue comme une source de coût, encore plus si elle n’est pas occupée. Les managers et clients cherchent donc à constamment les alimenter en tâches. Il est important de dégager du temps pour des activités annexes : refactoring, automatisation, veille technique, etc… Aujourd’hui, de plus en plus d’entreprises ont un jour par semaine dédié à ces activités.

Essayer de convaincre les gens avant de tenter quoi que ce soit est généralement un effort vain. Il ne faut donc pas avoir peur de prendre des initiatives, les résultats sont souvent plus parlant que les débats.

Merci à Ouarzy et Léna pour leurs retours, merci à Romeu pour cet atelier très instructif.

Vous n’êtes pas maître de votre code

La complexité métier

Dans mon précédent article, j’ai évoqué les raisons pour lesquelles il faut s’orienter ou non vers une architecture de type CQRS. Parmi ces raisons, la première que j’ai évoqué était le niveau de complexité du métier : plus le métier est complexe, plus CQRS devient pertinent.

Seulement, comment définir et évaluer la complexité métier de son application ?

La complexité, c’est quoi ?

“Complexité, n.f. : Caractère de ce qui est complexe, qui comporte des éléments divers qu’il est difficile de démêler” : définition proposée par le Larousse.

Cette définition met clairement en évidence une première notion, elle implique de fortes dépendances entre plusieurs éléments.

Chain texture

J’ai récemment pu assister au talk “Out The Tar Pit Vulgarized” de Romeu Moura où il est justement question de complexité logiciel. Il commence par y définir les termes simple et complexe :

  • Simple : Qui n’est pas composé, c’est à dire, qui ne fait l’objet d’aucune dépendance et d’aucune récursivité.
  • Complexe : Qui est composé, c’est à dire, qui fait l’objet de dépendances et / ou de récursivités.

Dans le monde de la finance, les intérêts simples et composés retranscrivent bien ces notions.

Le métier et sa complexité

On peut définir le métier d’une application par l’ensemble des règles fonctionnelles qu’elle doit savoir gérer. C’est la partie essentielle d’un logiciel, la raison pour laquelle il est développé. C’est également ces règles qui permettent d’évaluer la complexité métier d’une application : sont-elles composées ?

Know The Rules-stamp

Cependant il est important de ne pas confondre le métier tel qu’il existe dans la vraie vie avec le métier tel qu’il doit être traité dans l’application. Si vous appliquez le Domain Driven Design, vous allez vouloir expliciter dans votre code le métier et ses règles, notamment au travers de bounded contexts et leurs ubiquitous language respectifs. Cette démarche n’a pas pour but de refléter avec exactitude la réalité, au contraire, elle encourage à utiliser une abstraction adaptée au problème que l’on souhaite résoudre.

J’aime beaucoup cette courte vidéo de Scott Millett qui explique très simplement ce qu’est l’abstraction d’un domain. Dans cet exemple, il montre qu’un plan de métro est une abstraction de la réalité (le réseau) adaptée pour un problème donné : savoir comment se déplacer d’un point A vers un point B.

Une autre forme de complexité

La complexité métier ne reflète pas toujours la complexité du code source : l’usage de langages de programmation, de frameworks ainsi qu’un mauvais design ajoutent un niveau de complexité supplémentaire, la complexité accidentelle.

Pour refaire le lien avec CQRS, l’intérêt est d’éliminer une trop forte complexité dans le code en utilisant des modèles de lectures et d’écritures adaptés aux besoins. Ces modèles sont des abstractions qui ne comportent que les éléments nécessaires à l’exécution d’une fonction, d’une règle métier. Leurs niveaux de compositions sont donc réduits à leurs minimums.

Une autre solution pour se protéger contre cette complexité accidentelle est l’architecture hexagonale.

Conclusion

La complexité métier est donc l’ensemble des règles métier et leurs dépendances. Plus il existe de dépendances entre ces règles, plus le métier peut être considéré comme étant complexe.

Merci à Ouarzy pour ses retours.

La complexité métier

Pourquoi utiliser CQRS et ES ?

Actuellement, j’entend de plus en plus parler de CQRS et CQRS/ES : par mes collègues autour de la machine à café, lors d’entretiens techniques, sur Twitter, les blogs, etc.

musthave

Le principe du Command and Query Responsability Segregation (CQRS) est de séparer modèles d’écriture et modèles de lecture. L’Event Sourcing (ES) quant à lui consiste à sauvegarder des événements au lieu d’entités, pour reconstruire une entité il faut agréger des événements. Exprimés de cette façon, ces concepts semblent plutôt simples à comprendre, mais les aspects techniques peuvent vite les rendre complexes à appréhender et implémenter.

Alors pourquoi choisir de modifier la façon dont nous représentons nos modèles de données ?

Si vous lisez attentivement un livre sur le Domain Driven Design (dont découle CQRS et ES), la réponse que vous obtiendrez sera : tout dépend de votre métier et de vos besoins.

Prendre une décision et décrire un changement

Pour comprendre un des avantages de CQRS, il faut se focaliser sur notre mode d’expression oral.

Imaginons qu’à un instant T je réside à l’adresse A, puis je déménage à l’adresse B. On peut dire qu’à T+1 je réside à l’adresse B.

Ici je représente une entité et mon adresse est une de mes propriétés. Un système de type CRUD (Create Request Update Delete) remplace mon adresse A par une adresse B, ce qui est fondamentalement vrai et simple à comprendre dans cet exemple. Cependant, le CRUD impose de me connaître en tant qu’entité : mon adresse n’est sans doute pas la seule chose qui me caractérise, on peut penser à mon nom, prénom, âge, sexe, taille, poids, etc. On observe une forte complexité accidentelle pour un changement d’état qui est pourtant simple. Mon poids n’a pas d’influence sur le choix de ma nouvelle adresse mais il est connu, et il doit être fourni lors de mon changement d’état.

De plus, avec le CRUD je ne mets pas réellement en avant l’action qui me fait changer d’adresse : mon déménagement. Les deux informations nécessaires pour me faire déménager sont mon identité et ma nouvelle adresse : ceci est ma commande “déménage” dans une architecture CQRS. Ensuite, mon identité et mon adresse actuelle sont sans doute les seuls éléments nécessaires pour prendre la décision de déménager. C’est une description partielle de mon état, mais adaptée à ma prise de décision, ceci est ma query dans une architecture CQRS.

Vu de cette façon, le CQRS semble donc plus proche de la façon dont nous raisonnons naturellement. On peut donc facilement exprimer les gestes métiers issus de l’Ubiquitous Language dans le code. Cette approche s’adapte bien avec une pratique comme le Behavior Driven Development (BDD), une action décrite dans un scénario de test se traduit naturellement par un commande envoyée au système.

Alors CRUD ou CQRS ? Quel est le niveau de complexité de votre métier ? Voici une réponse possible :

Complexité métier / Architecture

Simple Complexe

CRUD

Adapté

Complexité accidentelle

CQRS Sur-qualité

Adapté

La mémoire des actions

Est-il important de savoir quelles actions ont été menées sur votre système ? Cela peut être le cas dans certains métiers comme le e-commerce : ceci permet par exemple de savoir quels articles ont pu être ajoutés au panier puis retirés, et ainsi cibler les suggestions pour un client donné.

Le problème d’un système de persistance par état (utilisation d’entités) est qu’il n’y a pas d’historique des états précédents. En suivant cette logique pour mon déménagement, je sais à l’instant T je réside à l’adresse A. À l’instant T+1 je réside à l’adresse B mais je n’ai aucune trace d’un changement d’adresse.

Pourtant si vous me posez la question lors d’une conversation, je vais être capable de vous dire que je résidais à l’adresse A à l’instant T, et que maintenant en T+1 je réside à l’adresse B parce que j’ai déménagé entre temps. Notre mémoire fonctionne à la façon de l’event sourcing. Je retiens les événements qui me sont arrivés et grâce à eux je peux restituer mes états aux instants T et T+1.

events

Là encore, cette pratique s’adapte bien au BDD. Quand vous définissez l’état de votre système, vous décrivez les événements qui se sont produits.

Pour tester une architecture CQRS/ES avec le BDD, vous ajoutez donc un ensemble d’événements dans votre event store. Puis vous lancez une commande et vous vérifiez ensuite le comportement attendu (levé d’une exception, mise à jour des projections, etc.). Tester ce type d’architecture avec une approche métier est par conséquent très simple avec le langage naturel.

bdd
Un autre avantage de l’event sourcing est qu’il facilite la communication entre plusieurs contextes. Un bounded context peut émettre un événement dans un event bus, tous les bounded contexts qui attendent ce type d’événement le récupéreront et l’appliqueront à leurs propres modèles. Pour autant, il n’est pas nécessaire que ces deux contextes utilisent l’ES, une simple couche d’anti-corruption peut permettre d’interfacer un système de type CRUD.

Gagner en performance et en robustesse

Votre système a-t-il des attentes élevées en terme de performance ? On peut par exemple imaginer un site de billetterie en ligne, à l’annonce d’une date importante, celui-ci risque d’être pris d’assaut par les utilisateurs et nécessitent donc d’être robustes et rapides.

La majorité des systèmes ont un ratio lecture/écriture très déséquilibré, avec un nombre de lectures bien supérieur au nombre d’écritures. Gérer les relations entre plusieurs entités, notamment à l’aide de jointures, peut nécessiter d’importantes ressources et provoquer des latences.

C’est là l’un des autres avantages de CQRS, produire des modèles de lectures dédiées permet des requêtes rapides sans jointure. Chaque vue de votre application ne doit dépendre que d’un seul modèle de lecture, et ainsi effectuer une requête sur une seule table pour obtenir l’ensemble des informations qui lui sont nécessaires.

Pour rendre plus rapidement la main à l’utilisateur suite à l’exécution d’une commande, la mise à jour des modèles de lecture peut se faire de manière asynchrone. Il peut alors être nécessaire de mettre à jour les informations affichées pour assurer la cohérence des données avant que la commande ne soit réellement appliquée au modèles de lectures.

L’event sourcing permet également des gains de performance et de robustesse. Je parle ici du nombre d’opérations menées sur la base d’écriture. Un événement est un fait, il s’est produit et est irrévocable. Chaque événement est indépendant des autres, il n’existe donc aucune forme de relation entre les événements dans la base de données. On ne peut donc qu’écrire des nouveaux événements ou faire des lectures pour générer des agrégats.

Les événements suppriment également un problème inhérent aux modèles de données relationnels : vous allez devoir insérer ou mettre à jour plusieurs objets dans des repositories différents. Une écriture / mise à jour des données peut échouer en cours d’exécution, pour éviter une donnée partiellement enregistré, il faut alors mettre en place des systèmes de contextes. Ces mécanismes sont lourds à mettre en place et à gérer, ils ajoutent également une forte complexité accidentelle. L’ES vous affranchit des problèmes de cohérence des données en cas d’erreur lors de la persistance : l’écriture de votre événement fonctionne ou non.

Pour conclure

Bien que CQRS et CQRS/ES soient les nouvelles architectures “à la mode”, on constate qu’il ne s’agit pas de silver bullets : elles répondent à des problématiques précises. Il est donc important de clairement identifier ses besoins avant de se tourner vers ces architectures. Si vous choisissez de les utiliser, il ne faut pas les craindre : si celles-ci sont plus complexes à appréhender qu’une architecture en couche de type CRUD, les bénéfices compensent le coût initial de mise en place.

Merci à Nadège pour ses retours.

Pourquoi utiliser CQRS et ES ?

Les développeurs et le besoin métier

Développer est une tâche complexe, maintenir et faire évoluer un projet existant l’est aussi.

Une mauvaise qualité de code a de nombreux impacts négatifs : un nombre d’anomalies et de régressions affolantes, des coûts et délais exponentiels à chaque évolution, un manque de performances, voir une solution qui ne répond pas aux besoins. Le tout en sapant progressivement le moral des développeurs qui ont le malheur de travailler dans ces conditions.

Un problème de code

Le mauvais code peut prendre de très nombreuses formes, mais on retrouve souvent certaines caractéristiques :

  • Redondance
  • Faible consistance et absence de norme
  • Forte complexité cyclomatique
  • Fortes dépendances
  • Design chaotique
  • Ne révèle pas les intentions métier

Toutes ces caractéristiques rendent le code extrêmement difficile à lire et à comprendre. Comment déterminer ce que fait le programme en lisant le code ? Comment localiser une fonctionnalité ?

Un autre problème majeur est le turnover parmi les développeurs qui peut générer d’importantes pertes de connaissances s’il est mal anticipé : vous êtes parfaitement incapables de faire un lien clair entre un besoin, une fonctionnalité et son implémentation.

Dès lors, la moindre modification se fait à taton avec son lot de souffrance : effets de bords, incompréhension du code, régressions, etc…

Documenter, spécifier, recommencer

Une solution envisagée est de produire d’importantes quantités de documentation et de spécifications. C’est par exemple le parti pris des projets réalisés en cycle en V. L’idée est d’analyser le besoin et conceptualiser la solution à produire avant les développements.

cycle-en-v
Le premier problème est que plus une erreur est introduite tôt dans ce processus de documentation, plus les documents qui en découlent sont erronés.

  • Le périmètre doit donc être figé, sinon :
    • Les documentations sont très coûteuses à maintenir.
    • Les documentations deviennent rapidement obsolètes.
  • Les spécifications doivent êtres :
    • complètes
    • cohérentes
    • correctes
    • sans ambiguïté
    • réalisables

En principe, les développeurs ne réalisent que la conception détaillée, cette solution comporte plusieurs inconvénients majeurs :

  • L’architecture est imposée aux développeurs, et peut ne pas être adaptée.
  • Les développeurs sont focalisés sur les aspects techniques de l’application.

Le développement logiciel est une activité non-déterministe, par conséquent dans la très grande majorité des cas les développeurs rencontreront ces difficultés : aucune spécification ne peut être parfaite, il faut donc savoir improviser. Étant limités à une vision purement technique du projet, ils ne savent y répondre que par des solutions techniques sans aucun sens. Au fil du projet, ceci pollue de plus en plus le code et génère des deltas qui invalident progressivement les documents de référence. Un code illisible, une spécification qui ne correspond pas : vous avez de nouveau perdu les connaissances sur votre projet.

Le code, la seule vérité

S’il existe une vérité, c’est bien celle du code. Peu importe ce qui est écrit dans votre spécification, votre ordinateur appliquera ce que votre code lui dicte : le code fait foi, il est lui même la spécification la plus détaillée et la plus précise de votre programme.

Alors pourquoi ne pas l’utiliser comme tel ? Pourquoi ne pas s’efforcer à produire du code facilement compréhensible, facilement modifiable ? C’est pourtant ceci qui caractérise un code propre. Si celui-ci est expressif, alors n’importe qui (même une personne qui n’est pas développeur) peut le lire et comprendre les actions réalisées. Il est généralement accompagné d’un ensemble de tests unitaires qui expriment chaque cas géré.

strip-les-specs-cest-du-code-650-finalenglish

On peut voir le métier de développeur de beaucoup de manières différentes, il est souvent comparé à celui d’artisan (software craftsman), je le vois également comme un rôle de traducteur. Quand j’écris du code, je traduis dans un langage compréhensible pour ma machine un besoin qui m’a été exprimé dans un langage qu’elle ne comprend pas.

Mais expliquer quelque chose que l’on ne comprend pas soi-même est insensé. Il est donc primordial que les développeurs comprennent ce qu’ils développent, d’un point de vue technique, mais aussi d’un point de vue métier.

Améliorer la qualité

Pour écrire du code de qualité, il faut faire attention aux comportements, ne pas se contenter de quelque chose qui marche :

“How it is done is as important as getting it done.” Sandro Mancuso

Il est donc nécessaires de maîtriser et appliquer avec rigueur certaines pratiques et principes : TDD, SOLID, SRP, KISS, etc. Lire des livres tels que Clean Code de Robert C. Martin sont une bonne façon de les aborder.

Il faut ensuite travailler sur l’expressivité du code, du design. Est-ce que ma classe représente une notion métier ? Est-ce que ma méthode exprime une action métier ou technique ? Bien entendu, certaines problématiques restent purement techniques, mais elles doivent être les plus discrètes possibles dans le code en étant masquées derrière des interfaces dédiées.

Pour être expressif, encore faut-il savoir quoi exprimer. La meilleure façon est de s’intéresser au métier du logiciel. Discutez, même de façon informelle, avec l’utilisateur final, avec le product owner, avec l’expert métier : N’importe quelle personne pouvant vous aider à comprendre le problème auquel vous apportez une solution.

cameleon

Une fois que vous aurez compris le métier de vos interlocuteurs, vous serez capables d’échanger facilement avec eux, de challenger leurs besoins. Vous pourrez retranscrire les connaissances acquises dans votre code, celui-ci deviendra alors plus compréhensible, ses intentions seront beaucoup plus claires. En cas de doute, vous saurez également vers qui vous tourner pour répondre à vos questions.

Quelques méthodes

Il existe divers pratiques pour améliorer la compréhension métier des développeurs, et ainsi la qualité du code produit.

Adopter un fonctionnement agile est le premier pas. Ces méthodologies permettent de rapprocher développeurs et clients dans le but de faciliter dialogues et feedbacks. Mettre en place ce fonctionnement est un pré-requis à un certain nombre de méthodes de conception et de développement.

Le BDD (Behavior Driven Development) est une pratique intéressante à mettre en place. Il s’agit d’une variante du TDD qui met en avant le langage naturel et les interactions métier au travers de features découpées en scénarios d’utilisation. Idéalement, la rédaction de ces scénarios doit se faire avec un expert métier ou un product owner. Le développeur comprend alors clairement ce qu’il développe, et peut s’appuyer sur les notions, le vocabulaire employé dans ces features pour désigner son code. Cette pratique permet également l’émergence de l’Ubiquitous Language.

Enfin, le Domain-Driven Design. Il a été formalisé pour la première fois par Eric Evans dans son blue book qui présente un ensemble de patterns tactiques et techniques. Ces patterns couvrent l’ensemble du cycle de vie d’un projet : des méthodologies pour comprendre et représenter un métier, des choix d’architecture, de design, etc. L’idée est de produire une architecture qui présente de manière pratique plus que purement exhaustive les différents composants et interactions d’un domaine. Les points de complexité d’un logiciel doivent alors êtres des points de complexité métier et non techniques. L’arrivée de nouvelles pratiques comme l’event storming, ou d’architectures logiciel comme CQRS/ES découlent directement du DDD.

Pour quels résultats

Dans mon équipe actuelle, nous nous efforçons chaque jour d’appliquer ces principes et ces méthodes avec rigueur. Les bénéfices de ce travail se ressentent petit à petit.

La qualité de notre code augmente, le nombre d’anomalies est quasiment nulle. Étant bien découplé, et ainsi ne souffrant pas d’une forte complexité, notre code est également évolutif et peut subir rapidement des modifications qui peuvent être majeures.

Notre code fait foi : en cas d’un doute sur une question métier, le réflexe de tous (même celui du product owner) est de regarder le code. Nos documentations ne servent qu’à formaliser les futurs développements, et dans de rares cas à s’assurer qu’un morceau de code est bien conforme.

Merci à Ouarzy et Nadège pour leurs retours.

Les développeurs et le besoin métier

Le mutation testing

J’ai récemment lu un article de l’oncle Bob Martin, il y expose sa découverte du mutation testing et semble très enthousiaste à ce sujet. J’ai donc décidé d’essayer un outil pour mieux comprendre cette démarche.

Le principe

Aujourd’hui, beaucoup de projets sont réalisés en appliquant le TDD. Développer en appliquant le test first permet d’être sûr que l’on écrit uniquement le code nécessaire pour rendre un test valide.

Cependant, certains reprochent à cette méthode de mettre en évidence la présence de bugs, et non de démontrer l’absence de bug : un test qui échoue montre qu’il y a une anomalie, mais une anomalie peut exister sans qu’il n’y ait de test pour le montrer.

L’idée du mutation testing est de créer des mutations sur le code testé. Un outil analyse le code couvert par les tests puis génère des mutants : Mes tests sont-ils toujours vrais si je modifie cette condition ? Et si je ne fais pas d’appel à cette fonction ? Un mutant peut avoir deux états : mort ou vivant .

Les mutations peuvent prendre diverses formes : la modification d’une limite conditionnelle (< devient <=), l’inversion d’une condition (== devient !=), la suppression d’un appel à une méthode, etc.

Un mutant mort montre qu’au moins un test échoue si l’on modifie le code, on peut donc en déduire que les tests protègent bien le code contre les régressions. Un mutant vivant montre que tous les tests passent malgré une modification du code. Le mutation testing peut ainsi révéler que le code est mal protégé contre les régressions, il peut s’agir d’un problème de design ou alors c’est la qualité des tests qui peut être remise en cause.

Exemple

Pour mon exemple, j’utilise VisualMutator qui s’intègre directement dans visual studio.

Cas initial

Ici, je teste de manière laxiste une simple méthode qui me dit si mon objet Sequence contient un seul élément. Voici une première solution :

mutationtesting1

mutationtesting2Mutations

Après une première session de mutation sur mon code on constate des faiblesses dans mes tests :

mutationtesting3

Le mutant LessThanOrEqual me montre que je peux modifier ma condition tout en gardant mes tests valides. Je le constate bien si j’applique cette modification (< 2 devient <= 2).

mutationtesting4

Je peux ici rejeter la faute à mon dernier test qui fournit une liste de trois objets. Une fois corrigé je peux relancer un test par mutation :

mutationtesting5

mutationtesting6

On constate bien cette fois que la mutation LessThanOrEqual n’est plus vivante. Mais cette fois ci le mutant NotEquality reste vivant, il me manque donc clairement un test.

mutationtesting7

Cette fois ci je constate que mes mutants LessThanOrEqual et NotEquality sont tous les deux tués par mes tests.

mutationtesting8

L’utilité

Cette approche est clairement faite pour tester la robustesse des tests plus que le code en lui même. Elle permet de mettre en évidence les limites de notre jeu de tests, et ainsi la présence de potentielles anomalies non identifiées. En d’autres termes : Est-ce que je peux faire confiance à mes tests ?

Je ne suis donc pas convaincu que le mutation testing apporte une grande plus-value si le TDD est appliqué avec rigueur. J’avoue ne pas avoir su produire de mutant vivant sur un premier exemple écrit de cette manière.

Cette approche est donc beaucoup plus intéressante pour la gestion de legacy. Avant d’y apporter des modifications, mieux vaut écrire des tests pour se protéger contre les régressions. N’importe quel développeur ayant réalisé cet exercice sait qu’il s’agit d’une tâche complexe et qu’il est parfois difficile d’identifier tous les cas gérés. Utiliser le mutation testing peut facilement mettre en évidence ces cas non identifiés.

L’inconvénient

Il faut tout de même avoir conscience que cette méthode se révèle extrêmement coûteuse comparée à de simples tests unitaires. Il faut considérer le temps passé à l’analyse du code, à la génération des mutants, ainsi qu’à l’exécution des tests pour chaque mutant, ce qui peut prendre plusieurs heures sur un projet conséquent.

De manière grossière, imaginons un projet de 200 classes avec en moyenne 5 mutants par classe et un jeu de tests complet qui est exécuté en 30 secondes. On obtient :

200 * 5 * 0.5 = 8h20 (500 minutes)

Les tests utilisant la mutation ne peuvent donc pas être joués de manière systématique comme le sont les TUs. Il est selon moi beaucoup plus intéressant de l’appliquer de manière ponctuelle sur des régions ciblées du code.

Merci à Nadège pour sa relecture.

Le mutation testing

A whole team approach

Il y a quelques mois maintenant, j’ai eu l’occasion d’assister, grâce à un user group local, à une présentation de Sebastien Lambla. Celle-ci était intitulée Your Agile is Dead. J’avoue que je ne savais pas trop de quoi allait pouvoir traiter une présentation avec un tel nom, il s’agissait en réalité d’un retour expérience sur un projet réalisé en mob programming.

J’étais déjà convaincu par les bénéfices pour un projet d’appliquer le pair programming ainsi que les autres méthodes portées par l’Extreme Programming. Cette présentation m’a montré que l’on peut aller encore plus loin, j’ai donc décidé d’essayer de l’appliquer à mon équipe.

Le mob programming c’est quoi ?

“Mob” est le terme anglais pour désigner la foule. On peut présenter le mob programming comme une sorte de pair programming++ qui implique l’ensemble de l’équipe pour la réalisation d’une seule tâche.

WoodyZuill

Le Mob a été initiée par Woody Zuill. Elle découle d’un besoin récurent sur la plupart des projets : se réunir pour aborder des sujets complexes impliquant l’ensemble de l’équipe. Woody a alors décidé de réaliser ces réunions en y introduisant la notion de Driver / Navigator issue du pair programming. Cette pratique leur a immédiatement paru extrêmement pertinente et est entrée dans leurs habitudes.

Bien que je ne sois pas allé aussi loin, il est tout à fait possible de réaliser des projets entiers en mob programming.

Introduire le mob programming dans une équipe

Pour pouvoir l’appliquer, il m’a tout d’abord fallu convaincre de l’utiliser. Ceux qui ont déjà essayé de proposer le pair programming à des gens avec une vision (trop ?) “classique” du développement doivent imaginer ma principale problématique.

Toute l’équipe sur une seule tâche !? Tu veux ruiner notre productivité !

Mon discours était complètement informel, je me contentais de faire connaître la méthode autour de moi, à mes collègues, à mes pilotes, à mes managers.

Heureusement, tout le monde ne s’est pas montré aussi sceptique (pour ne pas dire réfractaire), d’autres se sont montrés plus curieux. C’était le cas de mon équipe avec laquelle nous sommes en accord sur les bonnes pratiques et les méthodologies de travail. L’amélioration continue fait partie de nos obsessions.

C’est lors d’une rétrospective d’équipe que nous avons décidé d’expérimenter le mob programming. Nous constations tous que le pair programming nous apportait de réels bénéfices, j’ai alors “officiellement” lancé l’idée. Comme nous sommes parfaitement autonome d’un point de vue opérationnel, notre pilote s’est vu obligé d’accepter malgré ses réticences du moment. De plus, nous avions une tâche idéale que nous souhaitions cibler pour cette expérimentation.

Notre expérimentation du Mob

Pour pratiquer le mob programming, mieux vaut être bien installé. Vous avez tout d’abord besoin d’un espace pouvant accueillir votre équipe, et assez calme pour pouvoir échanger facilement. Pour cela, une salle de réunion est sans doute la bonne solution. Il faut ensuite prévoir un poste de travail avec le support visuel sur lequel travailler : une télévision, un projecteur, … Il est important que tous les membres de l’équipe voient correctement ce qui est en train de se passer. Nous avions également plusieurs souris et claviers afin de facilement se passer la main. Ajoutez éventuellement un ou plusieurs laptop pour des tâches annexes telles que des recherches internet, consulter une documentation ou la rédaction d’un email. Et enfin, prévoyez un support sur lequel vous pourrez facilement représenter vos idées pour faciliter les échanges, un tableau blanc et quelques post-it feront l’affaire !
IMAG0427

Concernant mon équipe, nous étions cinq personnes avec différents profils :

  • un fonctionnel
  • trois développeurs
  • un recetteur

Pour nous faire une idée précise de la puissance du mob programming, nous avons choisi une évolution métier complexe à réaliser sur notre projet. Nous avons lancé la séance une fois que nous étions sûr d’avoir tous les éléments nécessaires pour réaliser la tâche demandée, aucun autre travail de préparation n’avait été réalisé en amont.

La première partie de la séance a donc consisté en une analyse du problème. Nous avons étudié les documents à notre disposition, regardé le code existant, débattu, schématisé, noté les points importants, … Ceci jusqu’à ce que tout le monde ait compris le besoin auquel il fallait répondre, ainsi que les tâches à réaliser. Cette étape nous a également permis de voir émerger un Ubiquitous Language au sein de l’équipe.

Est ensuite venu la réalisation. L’équipe a travaillé en TDD et BDD, nous avons traité les tâches les unes après les autres de cette manière, sans hésiter à lancer des micro-refactorings sur le code existant quand l’occasion se présentait. Même lors des phases de développement, la présence du fonctionnel et du recetteur s’est révélée très profitable. Cela leurs a permis de découvrir le red-green-refactor, mais aussi de challenger nos choix techniques.

Mais quand vous n’arrivez pas à tomber d’accord sur une décision technique, comment faites vous ?

C’est une question qui m’est posée de façon récurrente quand je parle du mob programming. Très honnêtement, nous n’avons pas rencontré cette situation. Une solution utilisée par Sebastien Lambla consiste à coder chacun de son côté la solution que l’on souhaite défendre pendant un temps donné, puis de comparer les résultats. De cette manière, vous révélerez plus facilement les avantages et les contraintes de chaque solution. Il vous faudra peut-être faire des concessions et accepter que votre solution n’est pas celle retenue.

Constats et résultats

Notre équipe s’efforce de travailler avec des approches DDD et BDD, le mob programming nous a permis d’aller dans ce sens. Une des principales problématiques dans le développement logiciel est la communication entre les différents métiers qu’il implique. Parce qu’ils ne parlent pas tous ensemble, et surtout pas tous en même temps, cela mêne à des incompréhensions et des lenteurs. Avec le Mob, vous enfermez les three amigos dans une salle pour résoudre un problème. Les échanges autour du besoin se font en direct avec des gens dont l’attention est entièrement portée sur ce besoin, vous êtes sûr de vous comprendre rapidement.

Comme nous avons été cinq à étudier le besoin, nous avons pu confronter cinq interprétations différentes des documents à notre disposition. Cet exercice est très intéressant à mener car nous avons immédiatement constaté des nuances dans nos compréhensions du besoin. Tant que vous n’arrivez pas tous à être d’accord sur le travail à réaliser, c’est qu’il existe une incompréhension qu’il faut résoudre, si vous n’y arrivez pas, c’est sans doute qu’il vous manque des éléments. A l’issue de notre séance, nous étions sûr d’avoir produit une solution adaptée au besoin.

Un autre avantage du mob programming est qu’il permet d’augmenter le bus factor de l’équipe à son maximum. Il s’agit du nombre de personne au deçà duquel votre projet n’est plus viable. Comme la totalité de votre équipe a travaillé sur le même sujet, la totalité de votre équipe possède la connaissance métier et technique. Pour perdre cette connaissance il faudrait que vous perdiez l’ensemble de votre équipe.

Même lors des phases de développement, notre fonctionnel et notre recetteur ont su challenger notre travail, en grande partie grâce à l’Ubiquitous Language et aux tests, mais aussi parce que nous avons fait l’effort de commenter à l’oral tout ce que nous écrivions. L’un d’eux s’est par exemple étonné d’une dépendance que nous utilisions dans une méthode, ce qui a révélé un problème de design dans notre code. La présence de trois développeurs à également de gros impacts sur la qualité du code, le principe est le même que pour le pair programming. Le code produit lors des séances de Mob est donc de très bonne qualité.

England v Australia - IRB Rugby World Cup 2015 Pool A

Notre productivité a sans doute été l’aspect du mob programming qui nous a le plus surpris ! Les solutions émerges très rapidement et l’équipe avance continuellement, un peu comme un bulldozer écarte tout ce qui se trouve sur son passage. Les séances sont très dynamiques, les idées fuses, il faut être capable de rester concentrer pour ne pas perdre le fil des événements. Le rythme peu même être tellement soutenu qu’il en devient épuisant, il est préférable d’aménager des pauses régulières, nous avons donc adopté la technique du pomodoro.

Le mob programming a également permis de resserrer d’avantage les liens dans notre équipe. Tout le monde est impliqué, tout le monde est au même niveau, tout le monde apporte sa contribution à une tâche potentiellement complexe, ce qui génère un sentiment de satisfaction général. De plus, c’est l’équipe et non un individu qui porte la responsabilité de ce qui a été produit, ce qui rend tout le monde beaucoup plus serein sans s’être déresponsabilisé pour autant.

Le Mob permet également de comprendre les problématiques de chacun. Mes problématiques de développeur ne sont pas celles de mon recetteur ni celles de mon fonctionnel. Travailler tous ensemble permet de mieux se comprendre, aborder les problèmes sous différents points de vues et ainsi de mieux avancer vers notre objectif. Cela permet également à chacun de partager ses compétences, son expérience, et ainsi de tirer le niveau de l’équipe vers le haut.

Nous avons donc été convaincus par l’efficacité du mob programming, les résultats obtenus ont créé une réelle émulation et cette pratique est entrée dans nos habitudes de travail. Si une tâche ou un problème complexe doit être traité, alors nous regroupons l’ensemble de l’équipe pour y répondre. Nos pilotes ont également été convaincus par les résultats obtenus et nous laissent désormais l’appliquer sans aucune réticence.

Le Mob, oui mais…

Je me dois d’apporter certaines limites au mob programming, il faut que certaines conditions soient respectée pour tirer les bénéfices de cette méthode de travail.

Le principale objectif du Mob est de pouvoir produire un logiciel de qualité :

  • qui répond de manière adaptée au besoin
  • dont le code source est propre et robuste

Si la qualité n’est pas votre priorité absolue, alors travailler en mob programming n’est sans doute pas la solution la plus adaptée pour votre équipe. Pour rappel, c’est parce que nous constations les gains de qualités obtenus grâce au pair programming que nous avons décidé de tenter l’expérience.

La grande force de cette pratique réside dans la communication et la collaboration qui sont grandement facilitées. Il me semble donc évident que votre équipe doit être désireuse de travailler ensemble, l’imposer à quelqu’un est un non-sens, et peut même se révéler pénalisant pour l’équipe. Les membres de l’équipe doivent également être ouverts à de nouvelles pratiques. Compte tenu de l’aspect “atypique” du mob programming, il ne faut pas avoir peur de changer certaines de ses habitudes de travail, ni de travailler devant ses collègues. J’ai pu animer plusieurs talks autour de ce sujet, je constate que ceux qui ont le mieux reçu le Mob étaient globalement des profils avec une bonne expérience de l’agilité. Pour cela, je pense qu’il est préférable que le pair programming soit déjà bien mis en place au sein d’une équipe avant d’aller plus loin.

Oui mais moi je suis dans une équipe de douze personnes…

Il y a effectivement une taille idéale pour un équipe qui travaille en mob programming, je pense qu’elle est de cinq ou six personnes. Cependant, il doit être possible de l’appliquer sur des projets avec des équipes plus conséquentes. Si c’est votre cas, vous ne travaillez sans doute pas tous sur les mêmes problématiques, vous pouvez alors identifier les personnes à regrouper pour travailler ensemble. Cela soulève des questions d’organisation.

La façon dont l’équipe est installée est un facteur clé ! Si vous n’êtes pas au calme, alors vous aurez des difficultés à échanger, vous serez interrompus. Nous avons constaté que, parce que nous étions tous ensemble isolés dans une salle de réunion, nous avons beaucoup moins été dérangé. Enfin, si tout le monde ne voit pas bien ce qu’il se passe à l’écran et ne peut pas intervenir à n’importe quel moment, alors vous risquez de perdre des gens en cours de route.

Si on pousse à l’extrême

Mon article décrit les impacts du mob programming lorsqu’il est appliqué ponctuellement. En y réfléchissant, je me rend compte que ces impacts sur le fonctionnement de mon équipe seraient bien plus profond si nous travaillons uniquement de cette manière.

Quelle utilité à maintenir le daily standup ? Idem pour nos tests croisés (revue du code d’un autre développeur avant de l’envoyer à la recette) ? Finalement, notre kanban ne devient-il pas à une simple todo list ?

Les exemples de ce genre sont nombreux, et remettent même en questions certaines pratiques de l’agilité. Voilà pourquoi Sebastien Lambla déclarait “Your Agile is dead” !

Merci à mes « co-mobeurs » Ouarzy et Nadège pour leurs reviews.

A whole team approach

Le code, c’est mieux à deux

Parmi toutes les bonnes pratiques mises en place dans mon équipe actuelle, il y en a une que j’apprécie tout particulièrement : le pair programming. Nous l’utilisons pour plusieurs raisons :

  • traiter des tâches complexes
  • faciliter les montées en compétence métier et technique des différents membres
  • augmenter la qualité du code

Je suis réellement convaincu par les avantages et les bénéfices qu’une équipe peut tirer du pair programming. Cependant, je constate que cette méthode n’est pas toujours bien comprise. J’entends régulièrement le même discours : si cette méthode marche “sur le papier”, elle ne semble pas “applicable en entreprise”.

Qu’est-ce que c’est ?

Le pair programming est l’une des pratiques portées par l’Extreme Programming, son principe est simple : deux développeurs travaillent en binôme sur la même tâche. On observe deux rôles :

  • Le driver qui écrit le code.
  • Le navigator / observer, il aide le driver en lui suggérant des solutions et en vérifiant le code au fil de l’implémentation.

Bien entendu, ces rôles sont régulièrement échangés. La règle d’or lors de ces échanges est : “déplacez le clavier, pas la chaise”. C’est pourquoi il est important que l’écran soit placé entre les deux développeurs pour faciliter les interactions et les échanges.

Une impression de “gaspillage”

Une crainte récurrente chez toutes les personnes qui n’ont jamais travaillé en pair programming est la perte de productivité : “Il y en a un qui écrit pendant que l’autre regarde.”, sous entendu celui qui n’écrit pas est passif.

Faineant
En vérité, si vous comparez la charge consommée par une personne et la charge consommée par un binôme, en effet vous consommerez plus. Il a ainsi été déterminé que des développeurs consomment environ 15% de charge supplémentaire lorsqu’ils pratiquent le pair programming, pourtant les bénéfices dépassent ce surcoût.

Pour calculer ce surcoût vous allez vous baser la grande majorité du temps sur des chiffrages, des estimations (Avez-vous vraiment fait deux fois le travail pour comparer ?). Selon mon expérience, les chiffrages sont par définition faux : ils se font généralement avec une vision idéaliste. Ils partent du principe que le besoin métier sera parfaitement compris et que la dette technique sera nulle. Ces deux facteurs pourtant majeurs sont généralement mal voire pas du tout pris en compte.

Ensuite, je trouve que s’appuyer uniquement sur ce 15% révèle une vision du projet à très court terme. Je pense qu’une charge de travail ne peut pas uniquement prendre en compte le temps de réalisation, il faudrait y ajouter le temps passé à corriger les anomalies ainsi que le coût de la dette technique engendrée.

Pour moi, il est donc beaucoup plus intéressant de comparer la qualité du code produit avec ces deux méthodes.

Faire les choses, mais les faire bien

Il ne faut pas oublier que ce sont bien deux personnes qui travaillent sur la même tâche, même s’il n’écrit pas, le navigator n’est absolument pas passif. La réflexion est partagée, les échanges sont dynamiques. Le fait d’échanger permet d’explorer plus facilement tous les aspects d’un problème. Cela a pour effet de dégager une compréhension claire du besoin et une solution appropriée de manière beaucoup plus efficace, tout en éludant plus rapidement les incompréhensions et les fausses routes. De plus, le navigator n’ayant pas à se concentrer sur l’écriture du code, il peut plus facilement prendre du recul et apporter un regard critique sur l’implémentation.

Le pair programming permet donc de proposer des solutions plus rapidement qu’un développeur seul. Il permet également d’améliorer la qualité sous deux aspects.

Tout d’abord, une quantité d’anomalies moindre. Une grande force du pair programming réside dans la revue continue. Quand vous écrivez du code, malgré toute votre bonne volonté, il n’est pas rare que vous fassiez des erreurs. Si la probabilité qu’une erreur vous échappe existe, la probabilité qu’elle échappe également à votre binôme est beaucoup plus réduite, elle sera ainsi signalée et corrigée immédiatement.

La qualité se retrouve également dans le design du code. Travailler à deux permet encore une fois de confronter sa compréhension du problème. Une solution technique qui peut sembler évidente pour une personne ne le sera peut être pas pour son partenaire, parce qu’il ne la comprend pas, ou parce qu’elle ne lui semble pas être la plus adaptée. Le pair programming fait émerger un meilleur design dans le code en confrontant les opinions et les expériences des deux développeurs.

Binome

Un autre avantage que je trouve au pair programming, c’est qu’il impose une grande rigueur dans le vocabulaire que vous utilisez. Employer une mauvaise notion conduit souvent à une incompréhension entre les deux membres. Comme il est important d’éliminer ces incompréhensions, vous allez attacher de l’importance à utiliser le terme, la métaphore la plus juste possible. Une fois la notion claire, explicitez la dans le code (réutilisez le terme employé à l’oral), vous dégagerez ainsi une forme d’ubiquitous langage. Cela rend votre travail plus simple à comprendre, et permet également à des personnes sans bagage technique de lire votre code. Ainsi, il nous arrive parfois de faire du pair programming avec notre product owner, il est parfaitement capable de comprendre ce que nous écrivons et de nous corriger si nécessaire.

Le pair programming permet donc de réaliser des tâches de manière plus rapide qu’un développeur seul, tout en garantissant un nombre d’anomalies plus réduit. On observe des gains à court terme avec moins de corrections à apporter suite aux développements. Le code étant plus propre, il est plus facilement maintenable et évolutif. Les gains se font également sur le long terme grâce à une dette technique réduite.

Certains comportements à éviter

Comme pour n’importe quelle méthode, il faut faire attention à certains détails lors de la mise en pratique du pair programming. Ici, le facteur humain et la bonne communication sont les deux clés pour assurer l’efficience de votre travail.

Quand vous travaillez en binôme, il faut faire attention au comportement de chacun. Si l’un des deux développeurs est passif, qu’il parle peu ou qu’il n’ose pas proposer des solutions, alors vous avez un problème. Le pair programming peut parfois générer de la défiance vis à vis de son partenaire. Il ne faut pas oublier de rester humble et de faire attention à son comportement.

Concrètement, travailler de cette manière va donner à votre binôme une vision claire de vos compétences, de votre façon de travailler. Si vous craignez de les exposer, c’est que vous avez probablement des choses à améliorer. Voyez le pair programming comme une excellente occasion pour progresser et vous ouvrir à de nouvelles pratiques, la séance n’en sera que plus profitable pour vous.

Nous avons tous une expérience différente, il faut en être conscient et veiller à ne pas s’enfermer dans une réflexion du type “il est trop fort, il est trop nul pour moi”. Tout d’abord parce que ce raisonnement ne se concentre que sur des compétences techniques purs. Comme je l’évoquais plus haut, la connaissance métier est essentielle pour répondre à un besoin. Si une personne est effectivement plus expérimentée, cela ne lui garanti pas d’avoir la meilleure compréhension du besoin, ni la meilleure solution technique pour y répondre. Personnellement, je travaille avec deux autres développeurs beaucoup plus expérimentés que moi, pour autant, je n’ai pas peur de proposer des solutions. Certaines sont retenues, d’autres non. L’essentiel est que toutes les propositions alimentent notre réflexion : Est-ce une bonne solution ? Pourquoi ?

ego-knowledge-ygoel-com_
Un autre comportement à éviter, et qu’il m’est déjà arrivé de rencontrer (à ma grande surprise ce jour là), est un esprit de compétition. Concrètement, mon collègue refusait ma solution qu’il jugeait pourtant comme étant bonne parce qu’elle ne venait pas de lui, nous avons perdu beaucoup de temps dans des débats qui étaient inutiles. Gardez à l’esprit que le pair programming vous fait travailler ensemble, pas l’un contre l’autre. Si un développeur se sent obligé de revendiquer le travail réalisé, alors il ne semble clairement pas fait pour travailler en binôme, ni en équipe. Lui imposer le pair programming ne sera absolument pas bénéfique pour lui, son partenaire ainsi que pour le projet.

Gagner en confiance

Quand je développe seul, il m’arrive régulièrement de me demander après coup “Est-ce que j’ai bien géré ce cas ?”, la grande majorité du temps oui, mais je me sens obligé de vérifier. Avec le temps, je constate que je suis beaucoup plus serein après une séance de pair programming. Mon argument est toujours le même : si quelque chose m’a échappé, il n’a probablement pas échappé à mon partenaire. Si effectivement je réalise que nous avons oublié un cas, j’en serai certain car je n’aurai aucun souvenir d’avoir échangé autour de son implémentation (Quel test écrire ? Quel design adopter ? …).

Avoir une telle confiance en ses collègues ne se fait pas du jour au lendemain, mais au fil des séances. Parce que vous apprenez comment l’autre réfléchit et procède, et parce que vous constatez qu’il est capable de déceler vous erreurs. Une fois cette confiance installée, vous redécouvrez le vrai sens du mot “équipe” : des gens avec qui vous collaborez et sur qui vous pouvez compter.

Merci à mes reviewers Ouarzy et Nadège.

Le code, c’est mieux à deux