Aller au contenu

Maîtriser la dette technique par les tests

Avatar de Pierre-Emmanuel Fringant
Publié le 22 mai 2023 Par Pierre-Emmanuel Fringant

C’est inévitable : chaque nouvelle ligne de code écrite dans un projet crée de la dette technique, c’est-à-dire qu’il faut la corriger si elle introduit un bug et la faire évoluer avec le projet. On peut limiter cette dette de plusieurs façons : en respectant les bonnes pratiques de développement, en rédigeant de la documentation, etc. ou encore en ajoutant des tests.

Nous demandons toujours à nos clients de « tester » ce que nous leur livrons, c’est-à-dire qu’ils doivent vérifier le bon fonctionnement d’une correction ou d’une nouvelle fonctionnalité en se connectant sur un environnement dédié, dit environnement de test ou staging. Plus exactement, ce que nous leur demandons ici, c’est de recetter notre livraison.

En informatique, un « test » a une définition bien précise : c’est une procédure de vérification d’un système, le plus souvent écrite dans le même langage de programmation que le code du projet lui-même.

Prenons un exemple simple : imaginons qu’un client nous commande un site internet permettant de calculer des additions.

Site fictif de calcul d'addition
Site fictif pour calculer une addition

Nous allons écrire dans le code du projet la fonction qui prend en entrée les deux nombres et qui donne en retour le résultat. Ce qui se passe à l’intérieur de cette fonction n’est pas important.

Code
fonction additionner(a, b) {
  // calcul...
}

Durant l’écriture de la fonction, le développeur va s’assurer qu’elle fait le bon calcul avec quelques nombres. Nous pourrions arrêter là, mettre le site sur un environnement de recettage, demander au client de recetter puis passer en production.

Cependant, on constate que ces procédures de vérification sont uniquement manuelles pour le développeur qui écrit le code comme pour le client qui recette. Sur cet exemple trivial, cela représente peu de temps, mais sur un projet réel, ces procédures de vérification peuvent prendre beaucoup plus de temps, être réparties entre plusieurs intervenants qu’il faut coordonner pour ne pas oublier des cas ou ne pas les tester en double. De plus, toute opération manuelle comporte un risque d’erreur dans la procédure de recettage elle-même.

C’est pourquoi nous allons introduire un test, une procédure de vérification de la fonction additionner. Les frameworks de développement modernes comme Ruby on Rails ou Laravel fournissent tous des outils pour écrire et exécuter de tels tests.

Le test que nous allons écrire est lui-même une fonction, sans paramètre en entrée, qui va appeler la fonction additionner du code fonctionnel avec deux nombres arbitraires, et vérifier que le résultat retourné correspond au résultat attendu. En pseudo-code, cela donne ceci :

Code
fonction test_additionner() {
  resultat = additionner(3, 2)
  retourne sont_egaux(resultat, 5)
}

Le développeur peut maintenant exécuter ce test n’importe quand dans son terminal, dès qu’il fait une modification dans la fonction additionner. Si la valeur retournée par la fonction change à cause d’une modification du code, l’exécution du test renverra une erreur.

Imaginons que notre client, lors de sa recette, s’aperçoit d’un bug : le site ne renvoie pas le bon résultat si un des deux nombres est négatif. Le premier réflexe serait de corriger la fonction additionner. Dans la philosophie des tests, on préférera commencer par écrire un nouveau test qui fait état du bug. Voici ce que cela pourrait donner :

Code
fonction test_additionner_negatif() {
  resultat = additionner(-4, 7)
  retourne sont_egaux(resultat, 3)
}

En exécutant tous les tests (on parle de « suite de tests »), le premier test passe, mais le nouveau ne passe pas, il renvoie une erreur comme l’a constaté notre client.

Le développeur peut maintenant s’atteler à la correction du code de la fonction additionner, jusqu’à ce que les deux tests soient exécutés avec succès. Il aura alors les réconfortantes certitudes suivantes :

  • il a corrigé le problème avec un nombre négatif, et ce bug n’arrivera plus ;
  • il n’a pas modifié le comportement correct avec les nombres positifs.

De plus, il n’est pas nécessaire d’inscrire la vérification avec un nombre négatif dans le cahier de recettage du client, ce qui serait le cas sans ces tests.

Les deux tests que nous venons de présenter sont des tests unitaires, qui vérifient l’exécution d’un petit périmètre fonctionnel.

Nous essayons de tendre vers une couverture de test (test coverage en anglais) de 100%, c’est-à-dire que chaque fonction comporte au moins un test, mais il peut y avoir comme dans notre exemple plusieurs tests unitaires pour une même fonction.

Dans le cadre d’un site internet ou d’une webapp, les fonctionnalités proposées au visiteur peuvent être composées de plusieurs étapes, par exemple un tunnel d’achat sur une boutique ecommerce. Dans ce cas, les tests unitaires ne permettent pas de tester le cheminement complet, on se tourne alors vers les tests d’intégration.

Un test d’intégration vérifie que plusieurs opérations s’enchaînent comme attendu, en simulant le comportement d’un internaute dans un vrai navigateur par exemple. Là aussi, les frameworks modernes proposent des outils pour créer des tests d’intégration.

Si l’on reprend notre exemple basique, un test d’intégration pourrait vérifier le bon fonctionnement global du site :

Code
fonction test_integration_addition() {
  appelle_url("/")
  remplit_champ("A", 15)
  remplit_champ("B", 4)
  clique_sur("Calculer")
  texte_est_present("Le résultat est 19")
}

Si l’une des étapes ne se passe pas comme attendu, le test échoue et renvoie une erreur nous indiquant à quelle étape il s’est arrêté.

Notez que le résultat du calcul nous importe peu ici, nous cherchons à vérifier que les interactions avec la page web se passent comme prévu, ce sont nos tests unitaires qui vérifient les calculs.

Dans le cas réel d’une boutique en ligne, dont le chiffre d’affaire fait vivre une entreprise, il est rassurant d’avoir la certitude que le test d’intégration du tunnel d’achat complet passe toujours après une intervention dans le code !

Les tests, unitaires et d’intégration, apportent confiance et sérénité lorsque plusieurs développeurs, débutants ou confirmés, travaillent sur le même projet. Chacun est certain de ne pas introduire de bug dans une autre partie du code, et les tests qu’il aura lui-même ajoutés en même temps que son code permettront aux autres développeurs d’avoir la même certitude.

L’ajout d’un nouveau développeur sur un projet qu’il ne connaît pas se trouve aussi simplifiée par la présence des tests. Même si les autres développeurs n’ont pas rédigé de documentation spécifique sur toutes les fonctionnalités, il est facile et rapide de se faire une idée des fonctionnalités existantes en lisant ce que « testent les tests ». La syntaxe même des tests les rend très lisibles et compréhensibles, même pour un développeur débutant.

Cette philosophie des tests nous a amené à déployer en production une nouvelle version du code à condition que tous les tests passent, de façon entièrement automatique lorsque la commande de déploiement est exécutée.

Cette méthode augmente encore la confiance dans notre code, et nous pouvons sereinement déployer en production plusieurs fois par jour si besoin lors des phases de développement actives.

Bien entendu, la conception et l’écriture des tests augmentent le temps de développement, de l’ordre de 15% à 20% selon les projets. Nous pourrions faire le choix de les proposer en option à nos clients, afin de réduire le coût initial et de livrer plus rapidement.

Cependant, notre expérience nous a prouvé que cette réduction initiale était très vite rattrapée par les coûts ultérieurs liés aux corrections de bug. Nous avons donc pris la décision de ne pas développer sans tests, en expliquant que le surcoût initial était vite amorti par le temps gagné en maintenance.

Le client lui-même économise du temps de recettage, car il n’a pas à tester toutes les fonctionnalités de base ni à reproduire les bugs corrigés à chaque nouvelle version du projet.

Prêt à travailler avec nous ?

Contactez-nous, ou venez nous rencontrer pour discuter de vos projets.