TDD : Test Driven Development – Partie 1 – Principes

Le développement piloté par les tests ou TDD "Test Driven Development" nous impose de commencer par écrire les tests unitaires avant le code de la vraie classe d'implémentation.

Ce fonctionnement apporte de nombreux avantages, mais est difficile à appréhender car il change notre logique d'écriture du code.

Nous avons à écrire le contrat que doit réaliser la méthode testée AVANT d'écrire cette méthode. Ce contrat est écrit dans le test unitaire et impose le comportement d'une méthode en fonction de données de départ.

Ce post a pour objectif de vous présenter les principes du TDD ainsi que le cycle de développement en mode TDD. Un second post vous présentera une démonstration du TDD.

Règles du TDD

Le fonctionnement en mode TDD impose le respect des trois lois suivantes :

  • Première loi : Toujours écrire le test avant d'écrire le code de la méthode testée.

Vous ne devez pas écrire le code d'implémentation cible tant que vous n'avez pas écrit le code du test associé. C'est à dire qu'il est nécessaire d'avoir imposé le contrat avant d'écrire le code cible.

  • Deuxième loi : Définir dans le test les comportements et les résultat attendus de la méthode testée.

Le code de test impose le respect d'une règle métier.
Ce test est en échec dans un premier temps : en effet, le code d'implémentation testé n'existe pas ou ne compile pas.

  • Troisième loi : Ecrire dans la méthode testée le code suffisant afin que le passage du test soit en succès.

Le code d'implémentation doit rester simple afin de gagner en efficacité. C'est pourquoi il est nécessaire de réécrire le code une fois que tous les tests sont en succès. De plus, les tests doivent rester en succès à la fin de la réécriture du code.

  • Quatrième loi : La couverture du code testé est à 100%.

La couverture du code à 100% permet d'éviter d'avoir du code non testé ou inutile qui peut être source d'erreurs.

Cycle du TDD

Le TDD repose sur les règles précédentes et permet d'avoir le cycle de développement suivant :

TDD - Cycle

  • Ajout d'un nouveau test / Modification d'un test en cas de changement d'une règle existante
  • Ecrire le contenu de ce test pour imposer le comportement à la méthode d'implémentation testée
  • Lancer tous les tests et vérifier que seul ce test ne passe pas
  • Ecrire le code minimal dans la méthode testée pour réussir le test
  • Lancer tous les tests et vérifier qu'ils sont tous en succès
  • Vérifier que la couverture du code testé est à 100%
  • Réécrire le code d'implémentation testé pour le nettoyer et le simplifier
  • Lancer tous les tests et vérifier qu'ils sont tous en succès
  • Vérifier que la couverture du code testé est à 100%
  • Recommencer ce cycle pour ajouter ou modifier un test

Ce cycle est à effectuer à chaque changement dans le code de l'application.

L'intégration continue permet de valider que le code d'implémentation respecte toujours les contrats imposés par les tests.

Conditions du TDD

Le développement en mode TDD impose les conditions suivantes pour être efficace :

Couverture à 100%

La couverture du code testée doit toujours être de 100%. Ceci permet de ne pas avoir de code non testé ou inutile.
Ceci est possible dans les classes non dépendantes d'un socle technique.

Dans eclipse, il est possible d'utiliser le plugin Eclemma pour vérifier la couverture du code à 100%.

Avoir des tests lisibles

Ce fonctionnement impose également de conserver du code de test propre, lisible et compréhensible en utilisant des noms de variable et une organisation compréhensibles.

Les tests unitaires deviennent la documentation fonctionnelle du code métier. C'est pourquoi le nom de la méthode de test doit être le plus explicite possible afin de savoir ce qu'elle teste sans même à avoir à lire le code. Il ne faut pas hésiter à avoir écrire le nom de la méthode sous forme de phrase ou avec le code de la règle métier testée.

Le test doit être unitaire

Les tests unitaires doivent s'exécuter le plus possible en dehors du contexte applicatif, c'est à dire sans avoir besoin de lancer le contexte Spring, d'avoir accès à la base de données ou à un serveur d'envoi de mails SMTP. Ainsi les tests pourront être lancés dans n'importe quel environnement.

Pour cela, le test doit rester unitaire, c'est à dire découplé du reste du contexte applicatif. Ainsi, dans le test, seule la classe testée est réellement instanciée, et toutes ses dépendances sont mockées. C'est à dire que les classes associées à la classe testée sont remplacées par des classes "mock" qui vont permettre de simuler le fonctionnement de leurs méthodes et de vérifier que ses méthodes sont bien appelées.

Le test doit s'exécuter rapidement

Le fait que le test est unitaire et découplé de l'environnement permet également de garantir que le test s'exécutera rapidement. En effet si l'exécution des tests est rapide, on aura plus tendance à lancer fréquemment l'intégralité des tests pour vérifier les non régressions.

D'autre part, les tests ne doivent par dépendre les uns des autres afin de pouvoir être lancés unitairement dans n'importe quel ordre. Ainsi un test ne doit pas dépendre du résultat d'un test précédent.

Le test doit échouer si une condition n'est pas remplie

Il est important qu'un test échoue dès qu'un comportement ou que le résultat n'est pas cohérent ou pas celui attendu. Ceci permet de détecter les anomalies le plus tôt possible. Ainsi il est nécessaire que le test unitaire remonte les exceptions au lieu de les gérer, de les mettre dans un fichier de log ou voir de les masquer. Ceci permet de les visualiser en cas d'échec.

Toujours écrire les tests avant le code

Il est important de toujours conserver l'ordre d'écriture : c'est à dire d'écrire les tests avant d'écrire le code d'implémentation.

Cet ordre permet de garder une dynamique dans le développement de l'application.

En effet, ceci garantit que le code d'implémentation sera toujours testable et ne sera pas trop complexe pour être testé. Cela évite également le découragement dans le cas où nous avons à écrire le test après avoir écrit notre méthode d'implémentation et que ce code est devenu trop complexe.

De plus, le fait d'imposer les contrats de la méthode testée rassure le développeur car le test garantit que le code développé fonctionne.

Bénéfices

Respect constant dans le temps des règles métiers

En écrivant les tests unitaires avant d'écrire le code de la méthode cible, cela nous oblige d'écrire chaque contrat dans ce test unitaire qui devient alors le garant du respect des règles métiers. Chaque méthode du test unitaire indique qu'une règle métier sera respectée par la méthode cible. Ainsi en cas de modification du code cible entraînant une échec du test, cela permet de détecter les régressions sans même avoir besoin de lancer l'application.

La plate-forme d'intégration continue permet cette détection à intervalle régulier et pour l'ensemble de l'équipe travaillant sur le projet. Le nommage de la méthode et de la classe de test doit être suffisamment lisible pour identifier la règle en échec sans même avoir besoin d'ouvrir la classe de test.

Simplification du code

D'autre part, le fait d'écrire le code minimal permettant de répondre au contrat décrit par le test permet la simplification du code.

Nous n'avons plus à effectuer de programmation défensive, ainsi pas besoin de tester le fait que tel argument de la méthode est null : les tests s'en chargent. Le seul cas où cela est nécessaire est s'il s'agit de méthodes de bibliothèques ou d'API publiques.

Nous n'avons plus tendance à ajouter de méthodes ou de classes utilitaires supplémentaires. Nous allons à l'essentiel, c'est à dire les éléments offerts par le langage Java et ses bibliothèques pour répondre aux besoins du test unitaire. Nous pourrons toujours réécrire le code plus tard, de toutes façons les tests sont là pour valider que le code répond toujours aux exigences.

Mise en confiance de l'équipe

Le fait que les tests garantissent le fonctionnement du code permet à une équipe d'être en confiance sur le travail réalisé par chacun.

Un membre de l'équipe pourra ainsi travailler sur le code d'un autre membre de l'équipe avec en garde fou les tests qui vont servir détecter les régressions. Ainsi cette personne sera davantage en confiance. Le fait que le test soit en erreur permet de dialoguer plus facilement entre les membres de l'équipe pour comprendre comment le code fonctionne réellement et ce qu'il est censé faire.

Amélioration technique

Les tests permettent de faciliter la réécriture et l'amélioration technique de l'application car ils permettent de lever des alertes si une modification a entraîné des régressions sur le fonctionnement interne de l'application.

Conclusion

Nous avons pu voir dans ce post les principes et le fonctionnement théorique du TDD qui apportent beaucoup de bénéfices notamment au niveau de la fiabilité et du respect des règles métiers.

Nous verrons dans un second post, une démonstration avec la mise en pratique des principes du TDD.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.