Menu

Dans un précédent article, j’évoquais le but des tests. Je soulignais qu’on ne doit pas tester la conformité d’un code avec une implémentation, mais avec un besoin. Je proposais une nouvelle unité de mesure, le need coverage, pour indiquer l’adéquation du code avec le besoin client. Mais un problème sous-jacent apparait assez vite avec cette réflexion : la personne qui porte la vision du produit, et donc qui est la plus consciente de ce besoin client, c’est le Product Owner. Comment lui permettre de vérifier la conformité de nos tests avec ce besoin ? C’est là qu’entre en jeu le Behaviour Driven Developpement, ou BDD.

Les critères d’acceptation

Souvent lorsqu’une User Story est rédigée, elle contient des critères d’acceptation. Ces critères permettent de définir formellement le comportement attendu d’une fonctionnalité. On les retrouve souvent sous une forme assez codifiée : “Étant donné queLorsque Alors …”. Par exemple, “Étant donné que je ne suis pas connecté Lorsque je clique sur le bouton Mon compte Alors je suis redirigé vers la page de connexion”.

Bien entendu, une user story contient bien plus que les critères d’acceptation, mais ceux-ci réunissent deux éléments importants. D’une part, ils sont une description directe du besoin utilisateur : c’est donc une excellente piste pour notre need coverage. Ensuite, ils sont structurés et codifiés de manière assez répétitive pour permettre de les automatiser.

En complément, il est important de noter que si la rédaction des User Stories est généralement effectuée par le PO, il n’est en rien le seul à pouvoir rédiger ces critères. Le produit est de la responsabilité de toute l’équipe. Le fait que le produit corresponde au besoin est donc aussi de la responsabilité de toute l’équipe… et donc ces critères également.

On est tous sur le même bateau

De story à test : le BDD

À ce point de notre travail, nous avons un ensemble normalement complet décrivant le besoin utilisateur d’une manière structurée. Il est temps de le mettre à profit : c’est là que le BDD entre en jeu.

Il existe plusieurs outils permettant de le mettre en place (Behave ou Cucumber par exemple) mais tous tournent autour de la même idée. Le principe de base est dérivé du TDD : on commence par écrire les tests, puis on écrit le code pour faire marcher les tests. La seule différence, c’est qu’ici, on ne va pas écrire du code pour faire nos tests. À la place, on va réutiliser nos critères d’acceptation.

Pour que nos tests soient efficaces, on va partir du principe qu’il existe un certain nombre de phrases prédéfinies. Ces phrases sont écrites en coopération entre tous les membres de l’équipe et peuvent comprendre des paramètres variables. On va utiliser ces phrases pour écrire nos critères d’acceptation (en les complétant si nécessaire). On va ensuite faire correspondre à chaque phrase un élément de code… et laisser la magie opérer.

It's a kind of magic !

Un exemple de BDD ?

Plutôt que de faire de longues explications, je préfère vous proposer un exemple. Imaginons une user story de ce type.

Plain Text

On notera des mots-clefs, Given, When et Then. Ces mots-clefs correspondent bien entendu aux éléments de nos critères d’acceptation. Nous allons ensuite définir nos phrases. J’utiliserais dans cet exemple des syntaxes inspirées par behave en python, mais l’idée générale est la même dans tous les cas.

Python

L’implémentation des tests n’est pas foncièrement différente de ce qu’on aurait pu trouver avec des tests classiques. Mais dans ce cas de figure, nos tests sont basés non pas sur du code, mais sur un élément commun à toute l’équipe, dev team comme PO : les critères d’acceptation. Ils relient directement le code au besoin utilisateur.

Avantage annexe, les phrases sont faites pour être réutilisées. Ainsi, lorsqu’on test des entiers, on pourra imaginer facilement deux cas particuliers : 0 et un nombre négatif. Sans avoir besoin de code supplémentaire, ces comportements, une fois écrits en tant que critères, deviennent des tests :

Plain Text
Misson accomplie

Erreurs communes

Le BDD peut être un véritable outil tant pour la qualité du produit que pour la cohésion de l’équipe, mais il peut aussi poser des problèmes. Voici quelques erreurs communes qui peuvent rendre son adoption plus complexe, voire contreproductive.

Trop de phrases

C’est la première et probablement la plus fréquente des erreurs en BDD. Reprenons nos tests de tout à l’heure, mais en les écrivant un peu autrement.

Plain Text

Le besoin utilisateur couvert est exactement le même. Mais ici, au lieu de trois phrases, on en a sept. C’est une tendance naturelle lorsqu’on écrit des critères d’acceptation : on utilise un langage naturel, pour avoir des critères qui nous parlent. Pour que nos tests en BDD puissent fonctionner, il faut lutter contre cette tendance et s’imposer une discipline. Notre dictionnaire de phrases doit être aussi limité que possible, et chaque nouvel ajout doit être réfléchi.

Des phrases trop techniques

Cette erreur a tendance à se produire quand le BDD est mis en place par l’équipe de dev sans impliquer le PO. On peut alors voir apparaitre des phrases de ce type :

Plain Text

Ici, paradoxalement, on aura probablement autant voir moins de variété de phrases que dans du BDD classique : si on peut définir une variable, appeler une fonction et lire une variable, on aura probablement déjà une part non négligeable de nos tests. Mais les phrases seront inutilisables par le PO. Ce ne sont alors plus des éléments de communication au sein de l’équipe. On garde l’avantage de la réutilisation des tests, mais c’est un bénéfice secondaire.

J’ai expérimenté ce type de tests sur un gros projet. Le résultat est marginalement plus simple au début, mais dégénère vite et devient illisible. Nous l’avions implémenté sur un microservice, et nous avons décidé de ne pas étendre l’expérience aux autres.

Faire des tests unitaires

Cette erreur est plus générale. Nous séparons généralement les tests en trois catégories : tests unitaires, tests d’intégration et tests fonctionnels. J’ai tendance à préférer nettement les tests fonctionnels / tests d’acceptation, mais je comprends le besoin de faire des tests unitaires. Dans certains cas, c’est d’ailleurs nécessaire pour des parties de code qui ne peuvent pas être testées autrement, même si cela devrait logiquement être exceptionnel.

Néanmoins même dans ce cas, les tests unitaires doivent être implémentés de manière plus classique, via pytest par exemple. J’ai déjà vu des tests unitaires implémentés via du BDD : cela donnera forcément le même résultat que celui de l’erreur précédente.

Par définition, un test unitaire n’est pas lié directement à un critère d’acceptation : nous sommes donc obligés d’utiliser deux méthodes de test différentes si nous voulons utiliser deux types de tests différents.

Et vous, vous avez déjà testé le BDD ?

La première fois que j’ai voulu écrire des tests unitaires pour un de mes programmes, ma première crainte était la masse de travail que le coverage allait représenter. Mon code faisait quelques milliers de lignes, je m’imaginais déjà passer des semaines à écrire tous les tests correspondants. J’ai donc écrit un premier test, pour vérifier que j’avais bien installé l’outil… et j’ai eu 73% de code coverage.

Le paradoxe du coverage à 100%

Ce type de paradoxe est fréquent lorsqu’on débute dans les tests. Créer des tests semble soudain plus rapide. On a bien entendu besoin d’un peu de temps pour réaliser les autres tests, couvrir les derniers pourcents, mais on y arrive. Finalement, le 100% final s’affiche, notre coverage est complet, on peut livrer le produit !

Victoire !

Le produit part en production, les premiers utilisateurs commencent à s’en servir… et les rapports de bugs pleuvent. Mais comment est-ce possible ? Nous avions 100% de coverage, chaque fonction est testée dans ses moindres branchements logiques ! Si cela vous rappelle quelque chose, et surtout si cela vous rappelle quelque chose de récent, l’explication est simple : vous ne testez pas la bonne chose.

Mais alors, que faut-il tester ?

Reprenons depuis le début. Pourquoi écrivez-vous des tests ? Après tout, c’est long, c’est pénible et soyons honnêtes, on a mieux à faire de nos journées. Alors, quel intérêt d’en écrire ? Trop souvent, la réponse sera de l’ordre de “c’est dans mon contrat” ou pire, “c’est nécessaire pour faire du code propre”. Dans ce cas, je dirais sans hésiter que pas de tests du tout sont mieux que des tests mal faits.

Une réponse partielle mais plus correcte serait “pour éviter les régressions”. Le code est (normalement) quelque chose de vivant : on ajoute des éléments, on en enlève, et chaque modification peut introduire un bug dans une fonctionnalité faite quelques itérations plus tôt. Des tests complets permettent de détecter une telle situation dès son apparition et donc de la corriger avant la production.

Mais cette réponse est incomplète, car elle se focalise sur le code et son contenu. Au risque de répéter ce que je disais dans mon article précédent, notre travail en tant que développeurs n’est pas d’écrire du code, c’est de résoudre des problèmes. Nos utilisateurs ont un problème à résoudre et nous proposons un moyen de répondre à ces besoins.

Notre code n’a pas d’autre usage que de fournir un service à l’utilisateur.

Écrire des tests, cela ne doit donc pas avoir pour but de vérifier que notre code est bien écrit, mais qu’il rend le service qu’il doit rendre. Sans ça, on tourne en rond en vérifiant que du code est bien un code correspondant à un bout de code, en perdant totalement de vue l’utilisateur final.

Et on tourne en rond encore et encore...

On teste un besoin, pas du code.

Donc, on écrit des tests pour vérifier que notre code réponde bien au besoin de nos utilisateurs. Cela peut paraitre un sophisme, mais c’est en fait un changement fondamental de notre mode de pensée. Si on teste que notre code réponde bien à un besoin utilisateur, alors la non-régression devient une évidence : on ne teste pas que notre code fonctionne toujours, on vérifie que même avec nos dernières modifications il continue de répondre au besoin exprimé. L’évolution des tests va de pair avec l’évolution du besoin utilisateur : si le besoin change, les tests changent. Si les tests changent, le code leur correspondant va probablement devoir changer également.

C’est cet état d’esprit, centré sur l’utilisateur et le besoin client, qui est à la base du Behaviour Driven Development. En mettant le besoin au centre du processus de tests, le test devient un outil de dialogue entre le PO et la dev team. En permettant une meilleure compréhension partagée de la story, il facilite les échanges et améliore le produit. C’est dans ce sens et uniquement dans ce sens qu’ils fournissent une sécurité au code. Je reviendrais sur le BDD et les outils pour l’implémenter dans un prochain article.

À l’inverse, des tests écrits parce qu’il faut faire des tests sont non seulement inutiles, mais peuvent nuire à la qualité du code en général. En effet, on va alors modifier notre code pour le rendre plus facilement testable, voir penser que nos tests remplacent le besoin client : s’il y a un problème, ce n’est plus que le code bug, c’est que le client l’utilise mal.

You're doing it all wrong

Code coverage et need coverage

La question du code coverage prend un sens tout particulier lorsqu’on l’aborde sous l’angle du besoin client. En effet, on peut ajouter un nouvel indicateur imaginaire : le need coverage. À quel point est-ce que nos tests couvrent bien le besoin client réel ? Avons-nous bien pris en compte tous les cas possibles en écrivant nos tests ?

Si oui, alors en toute logique nous devrions avoir un code coverage de 100%. En effet, notre code n’existe que pour répondre à un besoin client. Si la totalité du besoin client est couverte par nos tests, mais que notre code lui ne l’est pas, cela signifie qu’une partie du code sert à réaliser quelque chose qui ne correspond pas à un besoin client. En d’autres termes, c’est du code inutile, et il n’a pas à être présent dans notre application.

À l’inverse, avoir 100% de code coverage veut seulement dire qu’on a testé tout notre code. Rien ne nous dit qu’on a testé tout le besoin client. Les bugs qui apparaissent viennent systématiquement de cet écart. Un bug, c’est une chose qu’un client peut faire, mais que nous n’avions pas prévue. À nous d’atteindre les 100% de need coverage pour les éliminer.

Et vous ? Comment faites-vous vos tests ?

Lorsqu’on commence un nouveau projet informatique, une question qui revient parfois – en particulier si l’équipe est relativement jeune – est : « Quel langage / framework devons-nous utiliser ? Quel est le meilleur langage de programmation du moment ? ». On entre alors dans un long débat ; tel langage contient plus de modules et a une communauté plus grande, tel autre est plus performant ou propose une meilleure CI/CD.

À la recherche de la perfection

En tant que lead dev, j’ai parfois dû répondre moi-même à cette question. Lorsqu’un nouveau projet était lancé dans le groupe, on attendait de moi que j’indique la meilleure voie à suivre, celle qui allait garantir le succès du projet. À l’inverse, j’ai aussi eu l’occasion de voir (et de subir) les effets d’un mauvais choix et donc d’une mauvaise réponse à cette question.

Alors, quel est le meilleur langage de programmation et le meilleur framework ? Si vous êtes un manager sur le point de lancer un nouveau projet, vous êtes au bon endroit : je vais vous donner LA réponse à cette question. Si vous êtes développeur et que vous débattez avec vos collègues pour savoir quelle stack est la plus optimisée, bienvenue également !

Je pourrais vous proposer des graphiques avec le nombre de réponses StackOverflow par langage pour parler de la popularité des langages, comptabiliser le nombre de paquets dans le gestionnaire de paquets du langage, le nombre de stars github pour chaque framework, vous faire un comparatif des performances dans un superbe benchmark couvrant toutes les situations… mais ce ne serait pas une bonne manière de répondre. Toutes ces façons de comparer des langages viennent d’une incompréhension profonde, mais trop répandue, sur le métier de développeur.

Le secret des développeurs

Le vrai métier de développeur

En tant que développeur, notre métier n’est pas d’écrire du code, mais de résoudre des problèmes.

Si vous êtes payé au nombre de lignes de code que vous écrivez, ou si vous pensez que votre productivité s’évalue au nombre de commits faits par jour, alors votre employeur (ou vous) n’avez rien compris à ce que vous faites. Je peux écrire un code de 8000 lignes pour faire un Hello World. Ça n’aura aucun intérêt, ce sera lent, plein de tests idiots, mais c’est techniquement réalisable. Ce n’est pas pour ça que cela aura de la valeur. À l’inverse, un jour un collègue avait une base de données de plusieurs milliers de lignes dans un format inutilisable pour lui ; avec un script de quinze lignes j’ai transformé cette base pour qu’il puisse l’intégrer à ses outils et s’en servir et je lui ai fait gagner deux mois de travail de conversion manuelle.

Alors, en quoi est-ce lié au meilleur langage de programmation ? En tout. Si notre métier n’est pas d’écrire du code, alors notre choix de langage ou de framework ne doit pas être basé sur la vitesse du framework ou le nombre de sucres syntaxiques abscons permettant de faire les choses de manière plus courte. En réalité, dans l’écosystème des langages de programmation moderne, les performances sont comparables lorsqu’on les regarde à une échelle humaine. En d’autres termes, même si d’un framework à l’autre on pourra avoir un facteur 2 ou 3 en termes de vitesse, la variation est trop faible pour être réellement notable par un utilisateur humain. C’est encore plus vrai lorsqu’on prend en compte le fait que la majeure partie des différences de performances sont liées à l’implémentation de l’application elle-même, pas du framework ou du langage.

Le nombre de paquets disponibles, d’extensions, de questions SO, toutes ces valeurs semblent au premier abord plus intéressantes : effectivement, si dans un langage il est possible de ne pas réinventer la roue, autant se diriger vers celui-ci, n’est-ce pas ? Mais c’est là encore un leurre. En réalité, si l’écosystème de chaque langage / framework est bien entendu très différent, la majeure partie des langages modernes proposent des solutions pour faire la quasi-totalité des applications de manière sensiblement similaire. Bien entendu, le fait de faire du web ou de l’application lourde aura un impact, mais pour le reste, que vous fassiez du Ruby, du Python, du PHP, du Rust, du Node, du Java ou même du R, vous trouverez un package permettant de faire une API, de vous connecter à une base de données ou de parser du JSON. Si votre cas est trop spécifique pour être présent dans un langage, alors il est probablement trop spécifique pour être présent tout court. Il existe quelques exceptions à cette règle (je pense par exemple à l’intelligence artificielle qui est principalement faite en Python et en Java) mais elles sont plus que rares.

Tout peut convenir

Faire un choix

Alors, quel critère retenir ? C’est très simple : celui de votre équipe. Apprendre un nouveau langage a un coût en temps. Il faut plusieurs mois, et même souvent plusieurs années, pour être aussi efficace avec un nouveau langage ou un nouveau framework qu’avec celui qu’on pratique quotidiennement depuis notre sortie du lycée. C’est du temps que vous ne passerez pas à développer de nouvelles fonctionnalités, à répondre aux besoins de vos clients, bref, à résoudre des problèmes. De fait, le meilleur langage de programmation, la meilleure stack, c’est celle que votre équipe connait et maîtrise.

Est-ce que cela veut dire qu’on ne doit jamais faire évoluer son équipe – et sa stack – vers de nouveaux langages ? Bien évidemment, non. Mais migrer vers un nouvel outil ne doit pas se faire parce que le nouvel outil est meilleur, mais parce que l’ancien n’est plus assez bien. Si vous trouvez que votre environnement actuel (ou celui de vos précédents projets) ne répond plus à vos attentes, vous ralentit et augmente la complexité de votre travail, alors oui, il est temps d’en changer. Mais dans ce cas, vous aurez une liste précise de besoins : pourquoi votre ancienne stack ne vous convient plus ? Qu’est-ce qu’elle rend plus difficile ? Qu’est-ce que vous devez améliorer ? Vous ne chercherez pas le meilleur langage, vous chercherez celui qui répond à vos nouveaux besoins. Vous pourrez de plus prendre en compte le temps de formation à ce nouveau langage dans votre vélocité.

Et vous, quelle(s) technologie(s) utilisez-vous sur vos projets, et pourquoi ?