À 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