Développement d’application Test Driven

TDD pour les intimes

Ecrit par Jérôme

Qu’est ce que le TDD

Le Test Driven Development (TDD) est une méthodologie de développement basée sur l’écriture de tests automatisés, notamment les tests unitaires, avant d’écrire le code métier. Cette approche, qui consiste à écrire un test, puis à écrire le code pour passer le test, vise à garantir que le code développé est entièrement testé et répond aux exigences du système.

En pratique, les tests ne cherchent pas à vérifier le fonctionnement du système dans son ensemble, mais ciblent principalement les aspects métiers de celui-ci pour pouvoir vérifier à tout instant la conformité des traitements et le bon respect des règles de gestion.

Comment bien concevoir les tests ?

Les tests unitaires valident le fonctionnement des différents composants ou modules d’une application. L’objectif est de s’assurer de la conformité des résultats produits pour différentes valeurs d’entrée (contexte). Ainsi, lors de leur conception, on cherche à identifier pour chaque composant, l’ensemble des scenarii retranscrits sous la forme de postulats :

Contexte + Évènement => Résultat attendu

Autrement dit, « si mon composant est dans un état donné et qu’un événement précis arrive, alors voici le résultat auquel je m’attends ».

Au moment de l’implémentation, chaque test unitaire doit être indépendant des autres ou de l’état du composant. Chaque postulat doit faire l’objet d’un test dont le résultat :

  • ne doit pas dépendre de la réussite ou de l’échec des tests précédents,
  • ni influencer les tests suivants.

Une autre façon de dire est que chaque test doit être parfaitement isolé des autres. Si l’on s’attend à ce qu’après un traitement ‘A’, un second ‘B’ soit exécuté, il est important de réaliser :

  • un/des tests pour ‘A’,
  • un/des tests pour ‘B’.

Il est également possible de tester l’enchaînement de ‘A’ et ‘B’, mais cela revient à tester un composant de plus haut niveau ou à réaliser des tests d’intégration et cela ne remplacerait pas le besoin de tester distinctement ‘A’ et ‘B’.

Pour faire une analogie avec le sport, le but d’un test d’intégration serait de vérifier que le participant ait bien franchi les lignes de départ et d’arrivée d’un marathon, et en combien de temps. Cela ne peut pas se substituer à la vérification qu’il ait bien franchi individuellement chacun des points de contrôle, faute de quoi il y aurait plus de cas de tricheries en faisant une partie du parcours en métro

Comment faciliter la mise en place des tests ?

Attention, pour faciliter le refactoring, il est essentiel que les tests se concentrent sur la validation des exigences et le respect des cas d’utilisation, sans tester les détails techniques de l’implémentation.

Une démarche TDD doit donc cibler la validation des composants d’un système sans chercher à valider leur fonctionnement interne. Les tests doivent garantir le respect du contrat d’interface de chaque module.

Une bonne conception doit ainsi découper le système en un ensemble de composants dont le comportement peut être testé et validé individuellement. Le changement de l’implementation interne d’un de ceux-ci ne doit pas remettre en cause les tests déjà implémentés.

L’architecture dite hexagonale est tout à fait adaptée à cela.

Plutôt que de vouloir adapter l’implémentation d’un traitement pour qu’il s’insère dans le reste du système, c’est au traitement d’imposer :

  • son interface d’entrée,
  • son interface de sortie.

Des couches d’adaptations pourront être ajoutées autour pour que l’existant réponde aux exigences des entrées/sorties du traitement.

Ainsi le traitement devient indépendant, par exemple, du choix d’une base de données (BDD) et de la structuration des données.

S’il devait y avoir un changement au niveau de la BDD, seules devraient changer les couches d’adaptation qui seraient alors mises à jour. Les tests et le code métier demeureraient inchangés.

Outre la flexibilité et la tolérance au changement, cela permet d’écrire des tests sans nécessiter d’avoir à sa disposition le système en entier, on injecte des éléments, répondant au contrat d’interface, sans être nécessairement des éléments finaux

Le principe d’imposer le respect de contrats d’interface, plutôt que de s’adapter à l’extérieur porte un nom: l’Inversion de Dépendance. Il s’agit d’un des piliers de base d’une bonne conception logicielle. Elle est à l’origine des architectures microservices.

Sans entrer dans les détails, il est important de comprendre que le TDD est une démarche qui s’inscrit dans une démarche plus globale de conception logicielle. Il est donc important de bien comprendre les principes de base de la conception logicielle pour pouvoir mettre en place une démarche TDD efficace.

Une conception inadaptée rendra la mise en place de tests unitaires difficile, voire impossible. Et chaque refactoring induira une charge importante de mise à jour des tests, ce qui va à l’encontre de la démarche.

Quand écrire le test ?

Dans le TDD, on considère que le test doit être écrit AVANT le bout de code qu’il contrôle ! On parle souvent du cycle Red, Green, Blue.

  1. Red – écrire le test unitaire pour une fonctionnalité que l’on souhaite développer, il doit échouer.
  2. Green – écrire le code minimal nécessaire pour que le test passe.
  3. Blue – refactoriser le code pour améliorer sa qualité, sans changer le comportement du composant.

A chaque étape, on s’assure que l’ensemble des tests unitaires passent, si ce n’est pas le cas, on revient à l’étape 2. Le processus de développement suit ainsi ce cycle itératif et incrémental pour l’ensemble des fonctionnalités attendues.

En écrivant les tests après le code, on augmente les risques de biais ainsi que la difficulté à identifier les erreurs. On risque aussi d’oublier des tests importants et d’alourdir la dette technique du projet.

Il est donc important de réfléchir aux cas d’utilisation qui n’ont pas été couverts par les tests existants et d’écrire des tests supplémentaires pour ceux-ci. Cela garantit que toutes les exigences du système et le code associé sont entièrement testés.

Enfin, les tests doivent être écrits en utilisant un framework de test approprié pour le langage de programmation utilisé. Les tests doivent être automatisés et être exécutés fréquemment pour s’assurer que le code est toujours fonctionnel.

Que faire si un bug arrive sans avoir été détecté ?

Comme pour tout déboggage, on commence par identifier les circonstances du bug, dans quel état était le système lorsqu’il est survenu. Un bug qu’on ne sait reproduire peut difficilement être corrigé.

On cherche alors à identifier :

  • quel est le contexte dans lequel le bug apparaît (les symptômes),
  • à quoi on devrait s’attendre.

On peut alors écrire un test unitaire dont on sait qu’il doit échouer au départ. S’il passe, c’est que les circonstances qu’on pensait avoir identifiées (le contexte) ne sont pas les bonnes. Une fois les préconditions vérifiées (le test échoue), nous pouvons commencer à écrire un correctif. Ensuite, nous pouvons exécuter le test, puis l’ensemble des tests préalablement écris. Il faut s’assurer que le correctif n’a pas eu d’effets de bord indésirables. On parle alors de vérifier la non-régression.

En conclusion

Il existe plusieurs avantages à mettre en oeuvre une approche TDD.

Tout d’abord, cela garantit que le code développé répond aux exigences du système. Cela signifie que le code est entièrement testé et que tous les problèmes sont identifiés et corrigés avant que le code ne soit publié en production.

Ensuite, le TDD facilite la modification du code et le refactoring. Étant donné que le code est entièrement testé, toute modification apportée au code peut être testée pour s’assurer qu’elle ne casse aucune fonctionnalité existante.

Enfin, le TDD contribue à améliorer la qualité du code développé. En écrivant d’abord des tests, les développeurs sont obligés de réfléchir aux exigences du système et à la manière dont le code répondra à ces exigences.

Ainsi, la démarche Test Driven Development, même si elle présente un surcoût lors de la mise en œuvre initiale, démontre rapidement son intérêt:

  • elle facilite la maîtrise de la dette technique,
  • elle améliore la qualité générale de la solution en réduisant le nombre de défauts,
  • elle accélère les évolutions en identifiant rapidement les régressions lors des phases de refactoring.

Retour en haut
Consentement à l'utilisation de Cookies avec Real Cookie Banner