En parcourant les vidéos récemment sorties de la Norwegian Developers Conference 2013, j'ai été interpellé par une présentation de Ian Cooper portant ce titre curieux : "TDD, where did it all go wrong ?". Si l'intitulé est accrocheur, le contenu s'est révélé tout aussi intéressant.

Real (TDD) men don't fake


Ian affirme qu'il s'est produit un moment dans la pratique de Test Driven Development où la notion de test unitaire a perdu son sens initial. Selon lui, Kent Beck a toujours employé ce terme pour désigner un test isolé des autres tests et non pas un test où le module de code testé est pris isolément des autres modules.

(c) christgr http://www.sxc.hu/profile/christgr

En d'autres termes, on doit faire en sorte que l'exécution d'un test n'ait pas d'effet de bord sur les autres tests et que leur ordre d'exécution n'ait pas d'importance, mais il n'est généralement pas nécessaire de focaliser notre test sur un objet en le coupant de son environnement (c'est à dire en mockant ses dépendances). Bien au contraire, ce procédé nous contraindrait à entretenir une pléthore de test doubles tous plus fragiles les uns que les autres. Dès qu'on change quelque chose dans l'implémentation d'une classe, une dizaine de mocks très liés à cette implémentation risque de ne plus fonctionner.

Voici en résumé l'approche qu'il conseille d'adopter avec les tests :

  • On ne doit pas tester des détails d'implémentation mais des comportements. Ce n'est pas la volonté d'ajouter une nouvelle méthode ou une nouvelle classe à notre système qui doit déclencher l'écriture préalable d'un test, mais l'apparition d'un nouveau besoin, d'une nouvelle fonctionnalité.
  • Il est difficile pour notre cerveau de penser à la fois à résoudre un problème et à le résoudre avec une solution robuste. Les deux premières étapes du cycle de TDD (Red/Green) devraient être consacrées à trouver une implémentation qui fonctionne, même effrontément stupide et mal bâtie, et ce n'est pas avant l'étape de refactoring qu'on devrait songer à améliorer la qualité de cette implémentation, à appliquer des design patterns, à séparer les responsabilités.
  • On a tout de même le droit, parfois, de s'aider de tests unitaires très proches des détails d'implémentation pour chercher la juste conception (Cooper appelle cela "rétrograder"). Mais ces tests sont fragiles et il est sain de les supprimer une fois implémenté le code qui les fait passer.
  • La majorité des tests unitaires devrait se focaliser sur les use cases qui se situent, dans une hexagonal architecture, à la lisière du noyau applicatif.



Mocking on Heaven's Door


La critique portée par Ian Cooper sonne comme une charge contre l'abus de l'approche opposée, connue sous le nom de mockiste.

Par exemple, à l'extrême inverse de ce que propose la présentation de la NDC, Joe Rainsberger professe, pour tester un graphe d'objets, de vérifier la basic correctness de chacun d'entre eux en empêchant tout effet de bord extérieur grâce à une barricade d'interfaces toutes mockées.

(c) mterraza http://www.sxc.hu/profile/mterraza

Pour Joe, les bugs d'un système peuvent être détectés et éradiqués d'une manière très simple - tester au niveau micro la robustesse de chaque objet en vérifiant non seulement s'il communique de façon valide avec ses collaborateurs, mais aussi s'il satisfait son contrat lorsque ce sont ses voisins qui le sollicitent. Pour cela, il faut des tests vraiment atomiques collant au plus près à la conversation entre l'objet et ses pairs, et non pas des tests d'intégration qui ont tendance à être lents, fragiles, très complexes si on veut être exhaustif, bref, une arnaque.

Le courant mockiste est d'autre part historiquement représenté par Steve Freeman et Nat Pryce, tenants de la London School of TDD et connus pour leurs travaux sur le framework d'isolation JMock ou encore l'excellent Growing Object-Oriented Software, Guided By Tests. L'usage des mocks préconisé dans le livre est beaucoup plus large et fondamental que ce que recommande Cooper, puisqu'ils vont structurer une partie de l'approche outside-in où l'on avance en implémentant au fur et à mesure les interfaces mockées dans les tests précédents. Cependant, le livre invoque par endroits le même slogan "Unit test Behaviours, Not Methods" que Cooper utilise dans sa présentation, ce qui semble indiquer un brouillage des frontières sur certains points.

Where did it go wrong again ?


Qui a raison ? Est-ce encore une querelle de chapelle stérile, la bonne réponse se situant entre les deux ? Si l'intervention de Ian Cooper abonde en points valides et plein de bon sens (sur l'utilisation prématurée de design patterns, la fragilité de certaines suites de tests à forte teneur en mocks...), elle soulève aussi beaucoup de questions.

  • Quelle peut être l'étendue d'un refactoring sur lequel on ne pourra pas ajouter de tests ? Est-ce qu'on peut faire émerger tout un nouveau module voire une nouvelle couche applicative sans la tester ? Est-ce que cela signifie qu'avant de commencer à écrire les tests unitaires, notre architecture doit être connue, stable et fixée pour empêcher le refactoring de la remettre en cause ?
  • Peut-on renoncer à l'efficacité d'un test unitaire à maille très fine qui va pointer directement vers l'objet défectueux lorsqu'un bug se produit, et nous épargner une séance de debug ? Est-il possible de se contenter d'un échec de test haut niveau accompagné d'une trace applicative parfois cryptique ?
  • L'intervenant reste finalement très vague et général à propos de "don't test implementation details". Qu'est-ce qui est considéré comme un détail d'implémentation ? Une classe auxiliaire ajoutée pour séparer les responsabilités ? Un objet représentant une composante d'une règle métier dans notre couche Domaine ? Tout ce qui est "en-dessous" des objets de plus haut niveau dans un module donné ? Où se situe la limite ?
  • Les problèmes de tests épousant de trop près les contours du code de production sont-ils dûs à la nature même de l'approche mockiste, ou à des défauts liés à l'utilisation parfois maladroite et/ou la rigidité de ces doublures dans certains cas, à savoir : mocks stricts obligeant à les spécifier intégralement, tests remplis de détails non pertinents pour la vérification à effectuer ? Ceci n'est-il pas réparé avec les évolutions apparues récemment (automocking containers, nouveaux frameworks d'isolation plus souples...) ?
  • Et c'est une question annexe, mais comment Cooper peut-il être sûr de bien résumer ce que pense Kent Beck ? Certains passages de Test-Driven Development By Example paraissent conforter sa thèse mais d'autres propos sont plus nuancés :

"TDD is not a unit testing philosophy. I write tests at whatever scale I need them to be to help me make my next step progress. [...] 40% of the tests of JUnit work through the public API. 60% of them are working on lower-level objects. [...] Part of the skill of TDD is learning to move between scales." (vers 22:00)

(c) klsa12 http://www.sxc.hu/profile/klsa12
Un autre développeur, Jason Gorman, semble choisir une approche pragmatique similaire sur laquelle on pourrait conclure :

Our goal here is reliable code and good internal design, which means that both schools of TDD are at times necessary - the Classic school when we're focused on algorithms and the London school when we're focused on interactions.