Test Driven Development : les bases

En test-driven development (TDD), on cherche à écrire du code directement testable; cela entraîne des conséquences immédiates sur la conception.

Le processus se présente ainsi :

  • on part d’une première fonctionnalité, assez simple, en se basant sur sa spécification; on écrit le code testant cette fonctionnalité et on l’exécute immédiatement : la fonctionnalité n’étant pas implémentée, le test tombe en échec (il est rouge)
  • on écrit ensuite la fonctionnalité, mais de façon très naïve, en ne codant que ce qui est nécessaire pour réussir le test (test vert) écrit précédemment (en appliquant le principe  YAGNI)
  • une fois le premier test vert, on peut s’attaquer à une seconde fonctionnalité (ou bien couvrir plus de cas de la première fonctionnalité); bien entendu, on écrit le test avant, on le fait échouer et ensuite on implémente jusqu’à le faire fonctionner
  • on itère plusieurs tours sur ce principe
  • on insère régulièrement des phases de refactoring permettant d’améliorer la qualité, la lisibilité et la pertinence du code ; les tests développés précédemment permettent de savoir immédiatement si on casse une ou plusieurs fonctionnalités et de corriger de suite
  • de manière générale, dès qu’un test est rouge on stoppe les modifications de code évolutives, tous les efforts de codage doivent être consacrés à remettre les tests au vert

Ce processus est également intitulé cycle red green refactor :

red_green_refactor

 

Le choix des outils de développement est également déterminant (un bon artisan-développeur sait s’équiper de bons outils), il est important de disposer :

  • d’un IDE permettant une manipulation facile et sécurisante du code et offrant des fonctionnalités puissantes de refactoring (ex : IntelliJ)
  • de bons outils de tests : des outils commes NCrunch (pour .net) ou Infinitests (pour Java) permettent d’exécuter les tests automatiquement à la moindre modification du code et d’offrir un feedback visuel immédiat sur l’état des tests (les tests en échec sont immédiatement repérables) et la couverture de tests (on voit immédiatement les lignes de code qui n’ont pas été parcourues par les tests)

Un des impacts directs sur la conception se retrouve dans le Single Responsibility Principle : on n’implémente qu’une préoccupation (métier ou technique) à la fois, de plus le refactoring fréquent fait émerger un découpage pertinent du code.

Le développeur obtient également un feedback constant sur son développement et sur l’état d’avancement (même s’il s’agit de baby steps : la prochaine étape devrait être aussi petite que possible), dans le cas de fonctionnalités assez complexes, le TDD aide à s’organiser et à garder le cap.

Code Kata : FizzBuzz

Voici un petit exercice pratique afin de se familiariser avec l’approche TDD (et dans une certaine mesure aux baby steps).

La fonctionnalité à réaliser est assez simple : il s’agit, pour un nombre entier n passé en argument, d’afficher les nombres de 1 à n avec les exceptions suivantes :

  • pour un multiple de 3, on affiche « Fizz »
  • pour un multiple de 5, on affiche « Buzz »
  • pour un multiple  de 3 et de 5, on affiche « FizzBuzz »

Exemple de sortie pour n = 20 :

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz

L’exercice consiste à développer cette fonctionnalité de façon incrémentale.

Dans un premier temps on se concentre sur l’affichage d’un seul nombre, et on applique le TDD sur les étapes suivantes :

  • affichage d’un nombre en chiffres, quel qu’il soit
  • gestion de l’affichage des multiples de 3
  • gestion de l’affichage des multiples de 5
  • gestion de l’affichage des multiples de 3 et de 5

Ensuite on gérera l’affichage de la liste des nombres de 1 à n.

Enfin, on ajoutera les fonctionnalités suivantes :

  • un nombre est « Fizz » si il est multiple de 3 ou si il contient un « 3 »
  • un nombre est « Buzz » si il est multiple de 5 ou si il contient un « 5 »

Quelques pistes pour démarrer

Pour démarrer l’exercice, on écrit un premier test.

Le premier réflexe va être de nommer cette méthode test1(). C’est pratique pour commencer mais ça ne veut rien dire.

Une bonne pratique est de nommer les méthodes de test de façon explicite en indiquant ce que la fonctionnalité testée doit réaliser (pratique inspirée du BDD, behavior-driven development). Voici ce que ça donne dans Eclipse :

buzzfizz1

Et là, c’est le drame, ça ne compile pas. Normal, puisque le composant testé n’existe pas encore. Qu’à cela ne tienne, nous créons la classe et la méthode correspondante :

buzzfizz2

Le test compile, et nous pouvons l’exécuter. Comme on peut s’y attendre, il échoue (mais ça fait partie du TDD) avec une erreur d’assertion :

expected: <1> but was: <null>

Nous implémentons la fonctionnalité, de la façon la plus simple possible (pensez au YAGNI), et relançons le test, qui cette fois-ci fonctionne :

buzzfizz3

Nous pouvons alors passer à l’étape suivante : afficher les multiples de 3. On évite de se précipiter sur la classe BuzzFizz pour compléter la fonctionnalité, et on ajoute plutôt un test supplémentaire :

buzzfizz4

Le nouveau test échoue, il faudra donc gérer le cas de la valeur 3, puis des multiples de 3, dans la classe BuzzFizz, et ainsi de suite.

L’exemple donné est très naïf : il serait nécessaire de couvrir davantage de cas, dans ces exemples on ne teste pas suffisamment de valeurs. Il faudrait ainsi tester davantage de valeurs entières dans le premier test, ou davantage de multiples de 3 dans le second (et même des non-multiples, pour s’assurer qu’on n’affiche pas « Fizz »).

Pour éviter d’alourdir le nombre de méthodes de tests, on pourra se tourner également vers la paramétrisation des tests.

Laisser un commentaire