Chacun cherche son TDD

Ce billet est le troisième d'une série sur la conception de logiciels[1].

Après avoir parlé de l'évolution d'un logiciel et de l'émergence des divers concepts qui le composent, voyons comment un développeur peut maitriser cette évolution. Le meilleur outil que je connaisse aujourd'hui pour cela, c'est le développement piloté par les tests[2].

Beaucoup de choses ont été écrites sur le TDD et ce billet sera donc un énième point de vue (le mien) sur le sujet.
Des reproches lui sont régulièrement faits : "le nombre de tests à écrire est trop coûteux", "le TDD est une mauvaise approche car il est inutile de tester toutes les fonctions/toutes les méthodes de toutes les classes", ...
Ces points de vue me donnent au moins une certitude : tout le monde ne met pas la même chose derrière le mot TDD.

En ce qui me concerne, le TDD me sert à deux choses :

  • 1- faire émerger et peaufiner les concepts d'un logiciel, aussi bien ceux d'architecture que d'implémentation
  • 2- vérifier le comportement des détails d'implémentation au travers des concepts

Le premier point est la conséquence directe de l'évolution naturelle d'un logiciel par conception émergente.
Regardons comment peuvent apparaitre des concepts d'architecture lors d'un pilotage par les tests.

  • 1- On a un test vide ;
  • 2- On commence à écrire le test : un début de concept apparait pour accéder aux détails à venir ;
  • 3- Les détails apparaissent. On passe alors par plusieurs phase red-green-refactor qui vont faire évoluer à la fois l'implémentation et le concept pour arriver à un résultat satisfaisant ;
  • 4- On introduit d'autres scénarios de test. Lors des red-green-refactor, d'autres concepts apparaissent ;
  • 5- Les concepts ont permis de diviser les détails. Une partie peut alors être avantageusement remplacée par une doublure pour favoriser la croissance ultérieure du logiciel.

3x01.png

L'apparition de nouveaux concepts d'architecture est le cas de figure idéal pour découpler les détails d'implémentation mais elle n'est jamais garantie.
Quand des détails deviennent trop complexes, il reste la possibilité d'introduire de nouveaux concepts d'implémentation comme dans l'exemple suivant.

  • 1- On a un test vide ;
  • 2- On commence à écrire le test : un début de concept apparait pour accéder aux détails à venir ;
  • 3- Les détails apparaissent. On passe alors par plusieurs phase red-green-refactor qui vont faire évoluer à la fois l'implémentation et le concept pour arriver à un résultat satisfaisant ;
  • 4- On introduit d'autres scénarios de test. Lors des red-green-refactor, le code se complexifie ;
  • 5- On travaille sur un nouveau concept d'implémentation pour simplifier le code. Pour cela, on rajoute des tests ad-hoc et on fait évoluer en parallèle le nouveau concept et son utilisation ;
  • 6- Le nouveau concept finit par remplacer complètement le code trop complexe.

3x02.png

Le point délicat de ces phases d'émergence est que, bien évidemment, les tests existants portent sur des concepts existants. Toute la difficulté réside dans la création de jeux de tests portant sur les nouveaux concepts. Cela nécessite soit de mener en parallèle l'émergence et la constitution des nouveaux tests, soit de les écrire a posteriori.
En l'occurrence, dans le premier exemple on n'a pas introduit de scenarios de tests utilisant le dernier concept créé pour vérifier les détails qui ont été séparés. Cela doit être fait immédiatement après pour permettre à ces détails de continuer à croitre sereinement.

3x03.png

Une caractéristique commune à tous les tests de ces exemples est qu'ils ne dépendent que des concepts, jamais des détails.
Pour moi, c'est un point important. Les tests sont écrits en utilisant des notions stables. Cela permet à la fois d'écrire des tests qui résisteront mieux à l'épreuve du temps et de les exprimer avec un vocabulaire clairement défini. Un test, ce n'est pas juste du code qui affiche une bande verte ou une bande rouge, c'est surtout un moyen de décrire intelligiblement le comportement du logiciel.

A l'usage, la manière d'écrire les tests diffère sensiblement selon que l'on manipule des concepts d'architecture ou des concepts d'implémentation.
Les concepts d'implémentation sont très proches des détails du code. Je trouve que les outils de type xUnit[3] se prêtent bien à l'écriture des tests qui y sont rattachés. Ils permettent de construire des concepts qui utilisent un maximum de possibilités du langage utilisé (par exemple, pour des fluent interfaces[4])

Les concepts d'architecture constituent le langage commun aux divers éléments d'un logiciel et, par extension, aux personnes qui participent au développement de ce logiciel. C'est en utilisant ce langage que l'on peut écrire des spécifications exécutables. Les concepts peuvent ainsi être paramétrés dans des outils de spécification exécutables de type 'scenario' (cucumber, specflow, jbehave...)
Typiquement, on pourra créer une bibliothèque de doublures dont les étapes d'initialisation seront utilisées dans les pré-conditions d'un scenario. On peut ainsi se constituer un "DSL" de spécifications pour notre logiciel. En se basant uniquement sur les concepts d'architecture, on peut écrire tous les scénarios utiles dans un langage naturel sans avoir à systématiquement détailler les étapes dans du code spécifique.
J'ai trop peu d'expérience avec les outils de spécification de type 'document' (fitnesse, concordion...) mais je suppose que l'on peut les utiliser de la même manière.

Emergence de la conception... Pilotage par les tests... Tout cela marche souvent très bien quand on écrit du code qui fonctionne en autarcie. Les choses peuvent être légèrement différentes quand il s'agit d'interagir avec des logiciels tiers
Dans le prochain billet, j'essaierai donc de parler des relations qu'un logiciel entretient avec son environnement.

J. B. Rainsberger

Merci pour ça. Bien écrit.

Si je comprends bien -- et le français n'est pas ma langue maternelle -- le deuxième scénario d'évolution correspond à ce que j'appèle "le problème Chunnel": c'est-à-dire que l'on construit deux parties du système séparément, et elles ne se connectent pas parfaitement, à cause de différences de conception. C'est probablement inévitable, mais pour ça je propose de pratiquer la conception entièrement "client-first". De cette manière, il est possible de diminuer la fréquence de ce problème Chunnel.

Oaz

Merci !

En fait, je ne me suis jamais vraiment posé de question sur ce "problème Chunnel". Peut-être devrais-je ?

Les 2 cas que j'ai essayé d'exprimer sont :

1) le cas où l'abstraction qui me manque pour exprimer le test m'apparait "naturellement" et là tout va bien : j'introduis l'abstraction et je continue la conception.

2) le cas où l'abstraction qui me manque ne m'apparait pas "naturellement" et là j'ai un code qui devient subitement trop complexe. Du coup je mets mon problème de côté et je démarre la création "bottom up" d'un nouveau composant qui m'aidera à simplifier le code initial.
Il peut donc y avoir, dans ce 2ème cas, un problème de connexion mais cela ne m'a jamais inquiété jusqu'ici car le "client" de ce nouveau composant est déjà là.

Oaz 12 novembre 2012 - 15:45

Fil des commentaires de ce billet

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.