lundi 17 février 2014

Par où commencer mon TDD ?

TL;DR.

Commencer par le service, caylebien.

Par où je commence ?

Dernièrement, je me suis posé la question sur la meilleure façon de commencer une implémentation en TDD sur un système plus trivial qu'un calcul de score de bowling. Un exemple : notre vénérable client souhaite disposer d'un service renvoyant la liste des collaborateurs travaillant au sein d'un service spécifique de l'entreprise. Plusieurs possibilités s'offrent à nous pour réaliser cette fonctionnalité.

3 possibilités

Les possibilités sont les suivantes :

  1. Je peux décider de commencer par les détails, au plus près de ma source de données et remonter petit à petit vers l'interface de mon service (une méthode getEmployees(businessUnitId) par exmple).
  2. Je peux déterminer quel sera le point central de mon design et développer de quoi le brancher d'une part au service et d'autre part aux sources de données.
  3. Je peux partir de mon service et développer brique par brique jusqu'aux sources de données.

Comme souvent, il n'y a pas d'approche parfaite. L'approche 1 permet d'avoir une bonne connaissance de la source et de tout de suite l'adapter pour le reste du système. Elle permet de ne pas avoir d'a priori en terme de design. Une fois parvenu a un niveau d'abstraction satisfaisant, on peut implémenter notre service. L'inconvénient est qu'il faut tout de même garder en tête le comportement attendu. Celui-ci doit guider nos choix sur l'exploitation de la source : quelles données dois-je remonter sur mes employés via la méthode getEmployees ? Un deuxième inconvénient est qu'en déroulant cette méthode, cela peut prendre un certains temps avant de rendre le service disponible.

L'approche 2 permet de se concentrer sur la partie qui nous semble la plus compliquée sur notre fonctionnalité. On fait le pari que l'intégration avec l'interface du service d'une part et la source de données d'autre part sera simple. Cela peut être une approche satisfaisante dans le cadre d'un projet waterfall traditionnel pour répondre à un problème de staffing : on va concentrer l'effort sur la partie complexe du système en faisant intervenir ponctuellement des développeurs plus expérimentés et ensuite réduire la charge sur le branchement avec le reste du système. On peut bouchonner une partie si besoin pour pouvoir témoigner de l'avancement par anticipation. L'inconvénient est qu'il est nécessaire ici de rester conscient des besoins à la source comme à la cible, d'avoir une bonne idée du design avant de commencer le développement. Enfin, nous prenons un risque sur une étape non négligeable de la réalisation : il est possible que l'intégration ne se fasse pas aussi simplement.

L'approche 3 assure que le comportement attendu soit assuré au plus tôt, même si l'implémentation de la fonctionnalité est totalement ou en partie bouchonnée. Pendant l'implémentation, le service demeure disponible. On peut même envisager du delivery en continu. L'inconvénient de cette approche est qu'elle implique de réfléchir un minimum au design au fil de l'eau. En effet, partir sur une implémentation naïve de bout en bout à l'issue de laquelle on prévoit de faire du refactoring n'est pas envisageable : d'une part, cela prendrait beaucoup de temps pour valider nos test et d'autre part, l'étape de refactoring serait par trop longue et décourageante. Au lieu de ça, l'implémentation se déroule avec l'objectif de valider chaque étape abstraite en simulant les parties plus concrètes via des bouchons ou des doublures (mocks). On opère ainsi de notre interface (le pus haut niveau d'abstraction) vers la source de données (le plus bas niveau d'abstraction). Cette façon de faire est facilitée avec l'externalisation de l'injection de dépendance, en utilisant par exemple Spring.

Commencer avec le service

En ce moment, j'ai jeté mon dévolu sur l'approche 3, galvanisé par la lecture de ce post. Voici grossièrement comment je procède.

Tout d'abord, je commence avec l'implémentation de la méthode du service. Ma méthode getEmployees doit me renvoyer une liste d'employés ? Pas de souci, je la crée et lui demande de me renvoyer une liste vide d'employés. C'est pas terrible mais cela permet de mettre le service en ligne et de laisser la possibilité à mes clients de travailler. Je me concentre sur les paramètre d'entrées et de sortie, implémente les paramètres obligatoire, créer les éventuelle exception à renvoyer et teste les cas générant ces exceptions. J'ai rapidement un début de service correcte que je peux conserver pour la suite de l'implémentation.

J'implémente ensuite un repository qui me ramènera mes objets en interrogeant mes sources de données. J'implémente les contrôles nécessaires et lui fait renvoyer une liste vide.

J'implémente alors l'injection de mon repository dans mon service. Pour ce faire, j'utilise une bibliothèque de mocks (comme Rhino Mock en C#), ce qui me permet de créer un test rapidement me permettant que contrôler que mon service utilise effectivement mon repository. Le seul point noir à mon goût c'est que je suis un peu forcé de créer une interface pour mon repository. En même temps, cela découple bien les choses.

Pour la suite, je continue de construire mon service en adoptant la même démarche pour mon repository, en utilisant des mocks si nécessaire pour faciliter le travail.

Conclusion

Voici, en vrac les points positifs et négatifs que je dégage de cette approche :

  • Le gros point fort est que l'approche est systématique : il n'y a pas besoin de réfléchir à l'étape suivante. On avance pas à pas en fixant le comportement attendu de la part des différentes couches.
  • On teste dans les moindres détails. L'approche TDD est facilité par les mocks qui fournissent des implémentations instrumentées plutôt pratiques pour nos interfaces.
  • La volonté de d'évoluer par petits incréments pousse à bien séparer les responsabilités. Il devient plus rare de revoir l'organisation des classes et la charge de refactoring se limite à l'organisation des méthodes à l'intérieur des classes. Cela permet de concerver une version fonctionnelle du code en évitant de péter nos interfaces tout en faisant du refactoring.
  • Je pense que les mocks sont très utiles à cette démarche. Il faut toutefois prévoir de choisir une bibliothèque qui convient et un petit temps de prise en main.
  • Il peut paraître frustrant de progresser par petits incréments et de bouchonner la Terre entière. En même temps, c'est du TDD mec.