Qu’est-ce que le TDD ?

Le Test-Driven Developement (TDD) est une méthode de développement qui a été formalisée par Kent Beck dans son livre Test-Driven Developement by example1.

Concrètement, le TDD consiste dans sa forme la plus simple à développer en respectant un cycle de trois étapes :

  1. Écrire un test qui échoue
  2. Écrire le code qui fait fonctionner le test
  3. Remanier le code

Souvent également appelées Red, Green, Refactor.

Kent Beck ne s’attribue cependant pas l’invention du TDD, expliquant que l’idée d’écrire des tests avant de développer lui a été donnée par un livre. Il parle donc d’une redécouverte2.

Dans cet article, nous allons explorer plus en détail les différentes étapes du TDD, puis nous explorerons quels sont les potentiels bénéfices que celui-ci peut nous apporter.

Le cycle du TDD

Pratiquer le TDD

Rouge

Pourquoi commencer par écrire le test ?

La première étape du TDD est d’écrire un test qui échoue.

Écrire le test avant le code nous apporte plusieurs avantages. D’abord, écrire le test nous force à réfléchir à comment nous allons appeler le composant qui sera testé, et donc à son interface. Quels seront ses arguments, quel sera son nom. Et donc, quelle sera sa responsabilité.

Le principe de responsabilité unique, ou Single Responsibility Principle est un principe décrit par Robert C. Martin dans son livre “Clean Code”3. Il s’agit du principe qu’une classe, méthode, ou n’importe quel autre composant d’un programme, doit avoir une unique responsabilité, c’est à dire une unique raison de changer.

En plus de nous aider à réfléchir à la responsabilité de notre composant, écrire les tests en premier nous aide également à écrire du code testable. Ceci peut sembler tautologique, mais je suis certain que nombre d’entre nous, après avoir implémenté une fonctionnalité, se sont aperçus que ce code était difficile à tester, à cause de différents problèmes comme des couplages forts et des responsabilités mal définies ou multiples.

Écrire les tests avant de développer nous encourage à écrire du code modulable, faiblement couplé, et donc plus facile à tester, à lire, et à maintenir.

Voir le test échouer

Votre test écrit, exécutez-le afin de bien le voir échouer.

En effet avoir un test vert n’est pas une garantie qu’il soit correct. Des erreurs s’y glissent régulièrement, et les plus difficiles à détecter sont les erreurs qui rendent nos tests verts pour la mauvaise raison.

Voir un test passer du rouge au vert lorsque nous implémentons la fonctionnalité désirée est une bonne manière de se protéger de ce type d’erreur.

Prenons ici un exemple simple. Une classe représentant un animal. Cette classe possède une méthode can_fly, nous retournant un booléen. Ce booléen est vrai si l’animal peut voler.

Test :

def test_ducks_can_fly():
    duck = Animal("duck")
    assert duck.can_fly

Code :

class Animal:
    def __init__(self, name):
        self.name = name

    def can_fly(self):
        return self.name in ["pigeon", "bat", "fly"]

Ici, notre test sera vert. Cependant selon le code, notre animal ne devrait pas pouvoir voler. Le test est vert, mais pour la mauvaise raison, et nous pourrions penser que la fonctionnalité est implémentée alors que le bug est dans le test.

L’erreur étant ici un oubli des parenthèses lors de l’appel de la méthode. Le test correct serait :

def test_ducks_can_fly():
    duck = Animal("duck")
    assert duck.can_fly()

De petits tests

Procédez par petit pas. Par exemple si vous implémentez une API HTTP, vous pouvez tester un appel simple à une route. Ou tester un appel où l’un des paramètres est manquant.

Écrire un test plus petit, en faisant attention à ne tester qu’une seule chose à la fois, a plusieurs avantages.

D’abord, les tests seront plus faciles à lire. Ne testant qu’une chose à la fois, ils seront plus petits et plus faciles à nommer d’une manière qui décrit sa raison d’être.

Ceci est également en accord avec un autre des principes décrit dans le livre “Clean code” : une assertion par test.

Nous obtiendrons une collection de nombreux tests faciles à lire. Ces derniers formeront une sorte de documentation de notre composant, mais qui ne pourra pas être rendue obsolète par l’évolution du code.

En effet un problème récurrent des commentaires ou documentations est leur tendance à ne pas évoluer lorsque le code change. Les tests ne peuvent pas avoir ce problème car une évolution du code qui les rendrait incorrects les ferait échouer, nous forçant ainsi à les mettre à jour.

Associé à de bonnes pratiques de nommage des tests, le TDD facilitera la compréhension du cas à corriger en cas de régression.

Le nommage des tests lui-même devrait également être plus facile si les tests sont de taille raisonnable, et ne testent qu’une seule chose à la fois.

Gagner en confiance

Enfin, nous devrions avoir une très forte couverture de code.

Toutes ces caractéristiques sont très appréciables en tant que développeur. Il est parfois compliqué de comprendre du code que l’on a écrit longtemps auparavant ou qui a été écrit par d’autres personnes. Il m’arrive donc souvent d’être hésitant, voire d’appréhender la modification de l’existant, par peur de ne pas parfaitement comprendre le code sur lequel je travaille et d’y introduire des régressions.

Une batterie de tests écrits en TDD retire complètement cette crainte. Peu importe si je n’ai pas parfaitement compris, je peux faire mes modifications avec l’assurance que si j’introduis une régression, un test la détectera.

Vert

Passons désormais à la seconde étape, l’implémentation.

Il est possible que votre test soit rouge, non pas parce que le composant testé ne possède pas encore la fonctionnalité désirée, mais parce que le code ne compile même pas. En particulier s’il s’agit du premier test d’un composant.

Commencez d’abord par faire compiler le code, en ajoutant les classes et méthodes nécessaires, mais avec une implémentation vide, retournant une valeur nulle ou absurde pour commencer.

Une fois que le test est rouge, mais que le code compile, il est temps de passer à l’implémentation de la fonctionnalité voulue.

Lors de cette étape, l’objectif est évidemment de faire fonctionner le test précédemment écrit, si possible de la manière la plus simple possible, avec un minimum de code. Il n’est absolument pas nécessaire d’écrire du beau code lors de cette étape. Faire des copier-coller, retourner des données en dur, tout est permis.

Les données en dur sont même courantes. Il s’agit d’une manière simple de faire passer le test au vert, et si le test est toujours rouge, c’est donc que c’est le test qui est incorrect. Ne vous inquiétez pas pour les valeurs en dur dans le code, elles disparaîtront à l’étape suivante.

Refacto

Une fois le test vert, passons à la dernière étape : remanier.

Cette étape de remaniement du code permet, selon Kent Beck, de supprimer les duplications. C’est donc le moment où l’on nettoie le code “sale” que l’on a pu produire dans notre hâte de faire passer le test au vert.

Il existe deux types de duplications. Les duplications dans le code lui-même, souvent à l’issue d’un copier-coller, mais également les duplications entre le code et le test. L’autre type sont les valeurs en dur que nous avons pu introduire. Par exemple, un cas trivial :

Test :

def test_sum():
    assert sum(3, 4) == 7

Code :

def sum(x, y):
    return 7

Ici, on voit bien apparaitre une duplication de la valeur 7 entre le code et le test, qui sera triviale à supprimer. Il est vrai que l’avantage de renvoyer une valeur en dur est discutable ici, et que si j’avais à implémenter cette somme en TDD je ne l’aurais pas fait dans un cas aussi évident. Cependant c’est une manière simple de se rassurer de l’exactitude du test, notamment lorsque les tests deviendront plus complexes.

Enfin, la dé-duplication n’est certainement pas le seul remaniement que vous puissiez faire. C’est lors de cette étape également que je relis mon code pour m’assurer qu’il correspond aux principes de clean code.

Et maintenant que le cycle est terminé pour notre premier test, nous pouvons choisir le test suivant et recommencer, jusqu’à ce que notre fonctionnalité soit implémentée.

Les avantages du TDD

J’ai parlé dans cet article des différents avantages que selon moi le TDD possède. Notamment le fait qu’il encourage un code de meilleure qualité, plus facile à lire et à maintenir. Mais qu’en est-il vraiment ?

Une étude a été faite sur 4 équipes, 1 équipe chez IBM et 3 équipes chez Microsoft, afin de regarder si le TDD apporte réellement des bénéfices en termes de qualités. Voici ce qu’il en ressort :

  • Une réduction du nombre de bugs entre 40% et 90%, avec une moyenne entre les 4 équipes de 67% de réduction de bugs, en comparaison avec des équipes des mêmes entreprises ne pratiquant pas le TDD.
  • Un temps de développement augmenté à court terme (entre 15% et 35%)

Cette autre étude se concentrant sur la perception du TDD par les développeurs, montre qu’après une période d’adaptation, les développeurs gagnent en confiance, et se sentent plus capables d’introduire des nouvelles fonctionnalités et évolutions.

Les limites du TDD

Le TDD, bien qu’ayant de nombreux bénéfices, n’est à mon sens pas suffisant pour produire du code de bonne qualité. Les principes décrits dans “clean code” restent pour moi tout aussi fondamentaux et ils se marient très bien à la pratique du TDD.

Une erreur fréquente du TDD est de tester une implémentation et non un contrat. Par exemple en écrivant une classe de test par classe, ou en testant directement des fonctions internes. Cela nous donnera des tests qui échoueront dès que l’architecture du code changera, même si les fonctionnalités restent les mêmes. Et nous devrons régulièrement corriger les tests.

Une description plus détaillée de ce cas, ainsi que des exemples pour l’éviter sont décrits dans les deux articles suivants:

Il est aussi fréquent de souffrir de la “vision tunnel” et d’oublier des cas lorsque l’on développe une fonctionnalité. Le TDD n’offre pas de solution à ce problème.

Enfin, comme vu ci-dessus, le TDD peut prendre plus de temps pour développer une fonctionnalité. Dans certains cas, par exemple le développement rapide d’un POC, celui-ci peut être un désavantage.

Conclusion

Le TDD est une pratique de développement nous permettant d’obtenir un code de meilleure qualité et plus facile à maintenir. C’est de mon point de vue particulièrement important au sein de Deepki, où nous développons une application possédant de nombreuses fonctionnalités. Notre base de code comportant des dizaines de milliers de lignes, il peut être intimidant, en particulier pour les nouveaux arrivants, et le TDD peut nous aider à mitiger cette complexité. Il est devenu la norme au sein de Deepki.

  1. Kent Beck, Test-Driven Developement by example, Addison-Wesley, 2002 

  2. Kent Beck, répondant à une question sur quora.com

  3. Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship, Pearson, 2008