lundi 28 avril 2014

On parle de TDD autour d’une bière …

Il y a quelques temps, j’ai eu des échanges très intéressants avec un copain qui m’interrogeait sur ma vision et ma pratique du TDD. Voici une retranscription un peu arrangée de nos échanges ...

Lui : Récemment, je me suis intéressé à des librairies Open Source Java, accompagnées de plus ou moins de tests. Le TDD permet-il de garantir que le code est bien testé ?

Moi : Il faut bien distinguer le TDD et les tests unitaires. Le TDD est une “pratique”,
une façon d’écrire le code et ses tests unitaires. Lorsque tu récupères du code, par exemple une librairie Open Source, ce que tu y trouves, ce sont des “tests unitaires”, écrit ou non en TDD, à priori tu ne le sais pas. Et pour répondre à ta question, la présence de tests unitaires ne garantit pas que le code est bien testé, ni que tout le code est testé. Mais, si le développeur a bien fait son boulot, normalement le code fourni est de bonne qualité, du moins de meilleure qualité que sans tests. Les tests unitaires présent garantissent un certain fonctionnement du code, et permettront plus facilement de corriger le code, voire le faire évoluer.

On dit que le TDD permet de mieux documenter le code, par l’exemple. Tu en penses quoi ? 

Là encore, ce n’est pas le TDD qui fera office de documentation, mais les tests unitaires, puisque c’est bien ce code qui est disponible et que tu peux relire. Je me répète, mais le TDD est une “pratique”, ça n’est pas un résultat qu’on peut ensuite consulter. Par contre, pour l’aspect “documentation”, je ne crois pas que le TDD garantisse une “bonne” documentation du code. Qu’ils soient écrits en TDD ou pas, si des tests unitaires sont mal écrits, s’ils ne sont pas simples, faciles à relire, ils n’aideront pas le relecteur à comprendre ce que le code fait. En revanche, une bonne pratique du TDD peut guider le développeur vers de meilleures tests, donc un gain sur cet aspect documentation.

Dans ton cas, est-ce que tu produis de la documentation, autre que les tests ? 

Déjà, concernant les tests unitaires, je m’efforce de les rendre les plus lisibles et explicites possible, je n’hésite pas à mettre en place un DSL (“Domain Specific Langage”) spécifique au contexte métier et fonctionnel de mes développements, avec une API la plus “fluent” possible. Mon objectif est que mes tests puissent se lire comme des phrases, comme un texte qui raconte une histoire. Ensuite, concernant les commentaires, je n’en mets pas. Tout comme Oncle Bob dans son livre “Clean Code” (que je recommande encore et encore ….), je considère les commentaires comme un échec de nommage et de présentation de mon code. J’ai une exception : je mets éventuellement de la JavaDoc pour une librairie “externe” qui sera utilisée par d’autres personnes, mais attention, le maitre mot pour une documentation : elle doit être utile et tout le temps à jour ! Et pour compléter le tableau de la documentation, je préconise quelques notes d’architectures, non pas rédigées avant les développements, mais rédigées pendant et après les développements, et surtout, maintenues à jour, pour présenter les grands principes d’architecture retenus pour le produit développé.

Et justement, question architecture, on dit que le design doit émerger grâce au TDD, qu’en penses-tu ?

Pour moi, il y a une mésentente sur le fait que l’architecture doit émerger grâce au TDD. Je suis convaincu qu’on ne peut pas développer une application de taille moyenne ou grosse, en partant d’un test et d’une seule classe, avec l’architecture de l’ensemble de l’application qui émerger petit à petit. A mon avis, de cette manière, on arrive à une architecture “spaghetti”. Ou alors, le développeur est très fort, ou plus probable, a en fait en tête l’architecture vers laquelle il veut aller.

En développement agile, on parle bien de “vision”. Il faut avoir une “vision” du produit qu’on veut développer, aussi bien au niveau fonctionnel (savoir dans les grandes lignes ce que devra faire le produit), mais également au niveau architectural, qui consiste à prévoir à grande échelle le découpage en modules ou librairies. Au démarrage d’un projet, je conseille aux équipes de réfléchir et définir cette “vision” de l’architecture à grande échelle du produit. Par contre, hors de question de définir, avant de coder, le design interne d’un module ou d’un package. Là, c’est bien le TDD qui va nous aider à faire émerger l’architecture, en partant des besoins fonctionnels, et des besoins en code testable. 

OK mais de quoi pars-tu dans ce cas ? 

Prenons le cas d’un package en Java. C’est un ensemble qui a une cohérence fonctionnelle, qui va donc répondre à un ensemble de besoins, cohérents entre eux. Cela va s’exprimer, par exemple, par la mise en place d’une API proposée par une classe du package, ce sera le “point d’entrée” du package. En TDD, on va donc pouvoir partir de cette classe et de son API pour commencer par écrire des tests, probablement en “mocking”, approche dite “London school”, en utilisant des mocks pour les 3 ou 4 collaborateurs de cette classe. Une fois que ces tests passent avec succès, on va pouvoir s’attaquer à chaque collaborateur qui vient d’être mocké pour écrire les tests de ce collaborateur et son implémentation, peut-être là encore avec l’usage de mocks. Et ainsi de suite, jusqu’aux “classes terminales”, celles qui n’ont pas de collaborateurs (les “feuilles” de l'arborescence des classes), et pour lesquels on écrit des tests unitaires “purs” (sans mocks). Pour moi, elle est là l’émergence du design par le TDD !

Est-ce que pour toi il y a un lien entre TDD et code propre ? 

Oui, bien sûr ! Lorsque j’interviens auprès d’une équipe, avec par exemple une formation “Tests unitaires et TDD” sur 2-3 jours, je leur parle de tests unitaires et de TDD, bien sûr, mais ces journées sont également le moyen pour moi de les interpeller sur pleins d’autres aspects du développement : le “Code Propre” (ou “Clean Code”), la présentation du code, sa lisibilité, sa simplicité et son expressivité, les commentaires (ou pas ...), les bonnes pratiques d’équipes, la maîtrise de son outil (IDE), les erreurs d’architectures, comme par exemple les problèmes liés à l’héritage, etc … Tous ces thèmes sont étroitement en relation et nécessitent d’être tous mis en oeuvre pour aller vers le succès, et le code propre est une des premières pierres !

A quel problèmes d’héritage fais-tu allusion et pourquoi ?

En programmation objet, on fait souvent un mauvais usage de l’héritage. Moi le premier, pendant des années, j’étais le “maître” en la matière, je produisais des architectures avec des héritages de folie ! Jusqu’à ce qu’un jour, un jeune collègue, plus jeune que moi (comment est-ce possible ? … ;-) ..) est venu m’expliquer que c’était mal … J’ai mis du temps à le comprendre et à l’accepter, mais il avait raison. 

Trop souvent, on utilise l’héritage pour apporter des fonctionnalités à un objet, c’est pratique. Mais c’est une erreur, car ces apports de fonctionnalités sont une facilité mais ne caractérisent pas réellement ce qu’est cet objet. C’est de “l’héritage technique”. La bonne solution n’est pas l’héritage mais l’aggrégation, c’est-à-dire fournir à l’objet un collaborateur qui va pouvoir l’aider dans ses tâches en venant avec ses propres fonctionnalités. 

Et concernant les tests unitaires, l’héritage est un véritable fléau et posent bon nombre de problèmes. Par exemple, pour tester une fonctionnalité héritée, dois-je le faire dans la classe fille ? Ça n'est pas logique et ça démultiplie mes tests, et pourquoi le faire plus dans cette classe fille là plutôt que dans une autre. OK, alors je teste la classe mère. Oui, mais elle est (souvent) abstraite avec plein de fonctions virtuelles … Voilà pourquoi lors de mes formations, je suis obligé d’aborder plein de sujets connexes aux tests !

L’héritage doit être utilisé à bon escient, avec modération, et beaucoup moins souvent que ce qu’on a l’habitude de voir. Lors de mes formations, je propose souvent aux équipes une architecture qui distingue les objets de traitement des objets de données (voir mon article "Développement et conception : mon approche P(N)OO"). Et avec cette approche, je ne fais jamais d’héritage sur les objets de traitement, et les héritages sur les objets de données doivent toujours respecter la règle “est un(e)”, ces objets de données ayant peu ou pas de traitement.

Pour en revenir au TDD, son but, c’est bien de tester le code, non ? 

En développement agile, l’approche consiste à ne pas anticiper sur les développements de l’itération suivante. Mais lorsque j’en serais à la prochaine itération, je devrais ajouter des fonctionnalités à mon produit, et mon code n’est pas forcément adapté. Pour ajouter ces nouvelles fonctionnalités, je vais donc devoir “refactorer” (remanier) mon code. Pour pouvoir faire cela en tout tranquillité, mon code doit être testé. Le meilleur moyen d’avoir du code testable et donc testé, c’est d’écrire d’abord les tests, le fameux TDD.

OK, donc le TDD, c’est du code testé et une émergence du design à moyenne échelle ?

Oui, mais pas seulement. Commencer par écrire les tests, c’est se focaliser sur ce que doit faire le code, sur le “quoi”. Sans le TDD, le développeur se plonge dans le code, dans le cambouis, il se polarise tout de suite sur le “comment”, et souvent, il fixe son attention sur les points qui vont être difficile à coder. Et finalement, il ne se pose pas vraiment, en profondeur, la question de ce qu’il doit développer, de ce que son code doit réellement faire. Moi le premier, j’étais comme ça. Se mettre au TDD implique un gros changement d’état d’esprit, cela nécessite des efforts, mais ensuite, tout parait plus simple, plus logique et tellement plus plaisant !

En conclusion, pour moi, le TDD est la meilleure pratique pour obtenir du code testé, évolutif et qui fait ce qu’il faut !


Merci à ce copain, qui préfère garder l’anonymat, pour ces échanges et pour sa collaboration à la rédaction de cet article. A noter que son niveau de connaissances (tests unitaires, TDD, architecture) est bien supérieur à ce que les questions ci-dessus peuvent laisser penser … ;-)

2 commentaires:

  1. Salut Xavier,

    Je partage ta vision. J'aime bien la distinction entre les tests, qui sont un résultat que l'on produit, comme le reste du code, et le TDD qui n'est que le processus pour y arriver.

    Mes divergences portent sur des points connexes :
    - Je ne fais pas (ou très rarement) de mocking à l'intérieur d'un package. Si j'ai besoin d'introduire une doublure, je prends ça comme un indice qui me dit que je suis probablement à la frontière du package. J'ai plutôt tendance à partir de "l'intérieur" pour faire émerger les frontières.
    - J'aurais une aproche un peu plus friande d'héritage. Toute instruction "if" peut cacher un polymorphisme qui se tranformera en héritage à la faveur d'un refactoring :-)

    RépondreSupprimer
  2. Très intéressant comme article.

    Par contre, je ne suis pas complètement d'accord avec ça :

    "En développement agile, on parle bien de “vision”. Il faut avoir une “vision” du produit qu’on veut développer, aussi bien au niveau fonctionnel (savoir dans les grandes lignes ce que devra faire le produit), mais également au niveau architectural, qui consiste à prévoir à grande échelle le découpage en modules ou librairies."

    Il est vrai qu'il est important de disposer à tout moment d'une vision architecturale du produit. Il est vrai aussi que changer fréquemment de vision est plutôt contre-productif, puisqu'on risque de passer plus de temps à refactorer le produit qu'à réellement développer de nouvelles fonctionnalités.

    Par contre, l'exemple du découpage est pour moi un mauvais exemple. Dans la pratique, c'est au contraire le genre de chose qu'il est très facile de remodeler à posteriori quand on pratique le TDD. Et inversement, c'est justement le genre de chose qui aboutit à des architectures trop complexes -et donc trop coûteuses vis-à-vis du but poursuivi- quand on y pense trop à l'avance parce qu'on ne fait pas de TDD.

    Pour moi, le vrai problème se situe surtout au niveau des frameworks utilisés. Si par exemple tu pars sur du Spring, il te sera très difficile de changer de technologie par la suite. C'est là que l'émergence de l'architecture par le TDD montre ses limites et que la vision initiale prend toute son importance.

    RépondreSupprimer