Test First : Une approche qualité

Publié le mardi 10 novembre 2020 à 16:12 , mis à jour le vendredi 13 novembre 2020
9 mins

Quelles sont les bonnes pratiques pour éviter les bugs ? Quelles sont les méthodes qui ont fait leurs preuves ? Avant de répondre à ces questions, nous allons voir qu’il est indispensable de bien comprendre pourquoi il vaut mieux prévenir que guérir.

1945 – Le premier bug

Depuis les débuts de l’informatique le bug est une source de problèmes. Le coût direct est important dans le process de développement, jusqu’à dépasser, dans certains cas, le coût de la phase de développement d’un produit. Le coût indirect (mécontentement client, image de marque …) bien que difficilement quantifiable est certainement encore plus important.

Les exemples de bugs aux conséquences catastrophiques sont nombreux dans l’Histoire de l’informatique. On pense tout de suite à la Nasa et l’énorme loupé de Mariner 1 en 1962, un bug à 18 millions de dollars (https://fr.wikipedia.org/wiki/Mariner₁).
Plus récemment, on se souvient de Toyota contraint de rappeler ses Lexus en 2009, pour un problème logiciel qui cause 400 morts et un coût estimé à 2,4 milliards de dollars. Des pertes financières astronomiques également pour LockHeed – le F35 est livré trop tôt (2016) : le debugging du système n’était pas fini, coût entre 20 et 100 milliards de dollars (http://www.extremetech.com/extreme/222380-the-pentagons-official-f-35-bug-list-is-terrifying).

À 50 ans passés, après plus de 30 ans de développement, il m’arrive encore régulièrement de faire des bugs. Même après de nombreuses années d’expériences, personne ne peut être infaillible. Comment concilier le fait qu’un créatif n’est pas parfait avec le besoin de l’entreprise de générer le plus parfait des codes ?

Grace Hopper aurait-elle pu éviter le premier bug ?

Bien sûr : il suffisait d’une moustiquaire et d’un peu d’attention.

Pouvait-elle imaginer le bug ?

C’est un problème classique, la poussière et les insectes sont les ennemis des plaques électroniques.

Aurait-elle bien réagit si sa direction lui avait reproché son erreur ?

Je ne le pense pas, il m’est arrivé dans mes précédents postes d’assister à des réunions où il était demandé aux développeurs de faire mieux leur travail, d’être plus attentif. Soit, mais l’erreur étant humaine, il est indispensable d’avoir des méthodes fiables pour les éviter.

L’architecture du projet

La démocratisation des architectures n-tiers et des modèles de conception a permis d’organiser, de créer et de modéliser les plans de test.

Il n’est plus concevable de générer un projet qui ne suive pas un modèle de conception (que ce soit MVC, MVVM …) et la plupart des projets utilisent un modèle en couches faiblement couplées, soit par choix conscient soit parce qu’ils utilisent un framework basé sur ces architectures.

Il serait dommage de ne pas profiter des avantages de l’architecture qui permet facilement l’implémentation de tests structurés.

Mike Cohn dans son livre « Succeding with Agile » explique les avantages d’une méthode automatique pour tester les projets. Il nous donne aussi les clés qui permettent facilement de séparer les différents types de tests.

Les tests unitaires

Ils sont chargés de tester les briques du projet de façon unitaire, ils sont indépendants des autres briques.

Dans une méthode classique ils sont peu ou pas fait, car ils touchent la base du logiciel, la partie qui est tellement simple qu’elle ne nécessite pas vraiment de test.

Mais si ça ne nécessite pas de test, pourquoi les faire me direz-vous ?

Pour éviter une constante en développement, la régression. Pas besoin de faire des tests quand vous écrivez ce bout de code qui est tellement simple, mais que se passera-t-il dans 2 semaines, 6 mois … Avez-vous pensé à un autre grand classique, l’effet de bord ?

Ils sont très simples à produire et à écrire, donc ils ne coûtent pas grand-chose si vous allez vers une automatisation des process.

Les tests d’intégration

Ils sont le pendant des tests unitaires. Ils vérifient que l’assemblage des unités de code (testées en unitaire) fonctionne comme attendu.

Les tests fonctionnels

Il simulent l’action d’un utilisateur et vérifient que l’attendu est bien égal à la réalité.

Les tests exploratoires

Pas encore de possibilité d’automatiser ce type de test. Peut-être un jour avec l’IA.

Extreme Programming

Fondé en 1999 par Kent Beck, Ward Cunningham et Ron Jeffries, cette méthode Agile apporte des principes intéressants dans la recherche qualité.

Les grands principes de la méthode sont les suivants :

  • puisque la revue de code est une bonne pratique, elle sera faite en permanence (par un binôme)
  • puisque les tests sont utiles, ils seront faits systématiquement avant chaque mise en œuvre
  • puisque la conception est importante, le code sera retravaillé tout au long du projet (refactoring)
  • puisque la simplicité permet d’avancer plus vite, la solution la plus simple sera toujours celle qui sera retenue
  • puisque la compréhension est importante, des métaphores seront définies et évolueront en concomitance
  • puisque l’intégration des modifications est cruciale, celles-ci seront faites plusieurs fois par jour
  • puisque les besoins évoluent vite, des cycles de développement très rapides faciliteront l’adaptation au changement

Nous pouvons voir que les notions d’intégration continue et de tests unitaires sont fortement liées à la méthode.

Ce sont les premiers à avoir mis en avant la notion de Test First et de Test Driven Development

Test Driven Development

C’est une méthode qui consiste à concevoir un projet de façon itérative et incrémentale. Elle consiste à écrire un test puis le code permettant de résoudre le test et à recommencer jusqu’à épuisement des conditions nécessaires au fonctionnement du projet.

Elle s’appuie fortement sur la philosophie d’extreme programming.

Les lois de la TDD

L’idée ici est de limiter les interactions entre le test et le code pour pouvoir concentrer le développement sur un cas simple et donc rapide à résoudre. Il faut donc respecter trois lois : la première est de ne pas écrire du code tant que vous n’avez pas écrit un test, la deuxième revient à ne pas écrire plus d’un test ne fonctionnant pas à la fois, et la dernière loi à suivre est d’écrire le minimum de code pour faire passer le test.

Le cycle de développement

En partant d’une user story (US) : On écrit un test qui décrit un seul cas de l’US, puis on vérifie que le teste échoue. On poursuit en écrivant le code nécessaire et suffisant pour que le test réussisse. Après avoir vérifié que le test passe ainsi que tous les tests précédents (non régression), on factorise le code en n’oubliant pas de vérifier la non régression. Il faut recommencer ce process jusqu’à ce que toutes les conditions d’acceptations de l’US soient couvertes.

Bien que cette méthode puisse paraître lourde, elle a des avantages certains, puisque le code est fortement testé et que la factorisation continue permet aux équipes de ne plus avoir peur du changement.

L’Agilité, une méthode fortement pragmatique

L’apparition de l’agilité a été un choc dans ma manière d’appréhender les problèmes : finie la notion du « dieu » chef de projet qui imposait une vision gravé dans le granite et les méthodes que les simples mortels (les développeurs) devaient appliquer.

Le développeur devient la pierre angulaire du projet, c’est lui qui décide et choisit les méthodes pour arriver au résultat demandé.

Je me permets donc de rappeler le dernier point du manifeste Agile : En revoyant régulièrement ses pratiques, l’équipe adapte son comportement et ses outils pour être plus efficace.

Les acteurs oublient souvent que cette règle s’applique aussi à la méthode. Modifier la méthode Agile pour coller aux équipes et au travail est normal. Ce n’est pas une méthode figée.

Il est donc possible d’étudier une méthode et de n’utiliser que la partie qui semble vous convenir.

De nombreux projets utilisent les méthodes développées par Extreme Programming, beaucoup sans même le savoir, certains sans même être agiles. Mais cela n’a pas d’importance, le principal étant que cela convienne aux équipes de développement.

De par sa structure, l’agilité nous demande de ne prendre que ce que nous pouvons/voulons faire et de l’appliquer à nos problèmes. Le process d’agilité étant auto correctif et auto évolutif, qui sait si vous ne finirez pas par appliquer toutes les méthodes d’extreme programming.

Le Behavior Driven Development

Créée par Dan North en 2003, cette méthode est communément nommée l’école londonienne. C’est une évolution du TDD.

Le principe est de partir de la demande utilisateur et de redescendre progressivement vers les couches basse de l’application.

Elle est aussi nommée l’approche de la boîte blanche, en opposition à l’école de Détroit qui part des couches basses de l’application pour remonter vers l’interface utilisateur et qui est nommée l’approche de la boîte noire.

Les grands principes du BDD

Il faut savoir que cette méthode permet la participation de personnes non techniques au projet. Les scénarios sont automatisés pour décrire des comportements, ce qui a l’avantage d’éviter les régressions et de pouvoir commenter le code. Comme on écrit les tests avant d’écrire le code, les équipes techniques vont mieux comprendre le comportement attendu.

Le cycle de développement

En partant de l’user Story (US) : Pour commencer, les conditions d’acceptations doivent inclure les tests fonctionnels qui seront ensuite implémentés. Ensuite on écrit : le code suffisant pour que les tests puissent compiler, les tests d’intégration induits par les tests fonctionnels, le code suffisant pour que les tests d’intégration puissent compiler, les tests unitaires induits par les tests d’intégration et pour terminer le code minimal afin de faire passer en premier les tests unitaires, puis les tests d’intégrations et enfin les tests fonctionnels. Si besoin on factorise à chaque étape pour que le code reste propre.

La convention est d’écrire les tests fonctionnels sous la forme : Given [état de départ] when [action] then [résultat] ou en français : Sachant que j’ai [état de départ] quand je fais [action] j’attend [résultat].

La méthode s’appuie fortement sur la notion de mocks (simulacres) qui sont des objets simulés qui reproduisent le comportement d’un objet réel. En effet, contrairement à la TDD, les tests unitaires et le code ne seront pas écrits dans un ordre logique, mais en BDD on considère qu’un test unitaire porte sur l’objet en cours et ne doit pas être dépendant des autres objets. Les tests de dépendances seront réalisés par les tests d’intégration qui eux n’ont pas de raison de tester le comportement des objets qui sont déjà testés.

Avantages de la méthode

L’un des avantages immédiat est que les tests peuvent être écris et compris par des personnes non techniques.

Le deuxième avantage moins apparent est que le cycle de développement peut être partagé entre plusieurs intervenants.

La rédaction des tests peut être réalisée par les PO ce qui leurs permet de valider la cohérence de la demande avec le client.

La première partie de l’implémentation peut être réalisée par une personne ayant une bonne connaissance de l’architecture pour respecter une implémentation et un découpage cohérent dans les divers couches de l’application.

La deuxième partie de l’implémentation ainsi que le codage peuvent être faits par des développeurs plus juniors car les objets sont déjà fortement positionnés dans l’architecture et restent donc très localisés.

Un autre avantage est la parallélisation possible du développement : une fois l’implémentation réalisée il est facile de faire les mocks des différents modules et donc de permettre à une équipe de travailler sur une partie du projet sans être bloquée par une partie non encore développée.

Bien sûr la méthode a des inconvénients

Contrairement à la TDD, le refactoring est plus compliqué. Cela donne une plus grande résistance aux changements.

Il est nécessaire d’utiliser, voire d’écrire, des mocks et donc de les maintenir.

TDD vs BDD ou Detroit vs Londres

Tests résistants aux changements : Comme les tests sont écrits du bas vers le haut de l’architecture, ils sont le plus souvent fortement découplés de l’implémentation. Une refactorisation importante a moins de risque de nécessiter la réécriture des tests.

Grande cohésion du code : Aussi bien la refactorisation permanente que le fait de progresser du bas (les tests spécifiques) vers le haut (les tests génériques) tendent à produire un code plus cohérent.

Peu de tests en double : Il est en effet peut probable qu’une même fonctionnalité soit testée deux fois car la première règle est que le test doit échouer et donc ce cas a peu de probabilités d’exister.

YAGNI : you ain’t gonna need it ou la probabilité est plus grande de fabriquer du code qui ne sera pas utilisé. C’est l’over-engineering classique des développeurs qui ont tendance à prévoir le besoin futur.

Formation : Il est nécessaire de former les équipes car la méthode n’est pas naturelle. Des rumeurs sur un turn over plus important des équipes de TDD circulent sur le net mais sans vrai fond.

Orienté utilisateur : La méthode parle aux utilisateurs non techniques

Appui fort sur l’architecture : Le fait de descendre vers le bas du modèle a tendance à éviter les erreurs d’architecture

Encourage la détection des effets de bord : Les effets de bord apparaissent plus tôt dans le cycle de développement et sont donc plus faciles à corriger ou à maitriser

Peu de code mort : La méthode même décourage le code qui ne sert à rien

Tests fragiles : Les tests sont plus difficiles à maintenir car il y a moins de découplage par rapport à la TDD

Refactoring difficile : les tests étant fortement couplés du haut vers le bas, le refactoring est plus lourd et plus difficile

Test en double : Certaines parties du code sont testées de multiples fois

Conclusion

Que ce soit le TDD ou le BDD la programmation test first donne du code de très grande qualité. Bien que l’écriture des tests soit importante nous avons tous tendance à la reporter à la fin du développement. Et comme la fin d’un cycle est souvent tendue les tests sont remis à plus tard (donc jamais, comme la documentation).

L’écriture de test dans le développement moderne n’est plus facultative, elle devient obligatoire, voire prioritaire. Combien d’effets de bord et de régressions auraient pu être évités par l’écriture de simples tests unitaires ?

Test First nous oblige à coder les tests en amont des développements et c’est une bonne méthode, mais d’autres sont possibles comme l’impossibilité de valider un rendu de code qui n’est pas couvert à plus de x% par des tests.

L’avantage de test first, c’est qu’au lieu de tester ce qu’on vient de coder (et donc de ne pas forcément tester ce qui est nécessaire), on code ce que l’on désire tester, le test est donc forcément probant.

Références

Cohn, M. (2010). Succeeding with agile: software development using Scrum. Addison-Wesley.