Mokona Guu Center

Avoir confiance en son code (et en celui des autres)

Publié le

Il est là, le bug, celui que tout le monde redoutais. Il se cache, il arrive parfois, pas tout le temps. Entre les programmeurs, la suspicion s'installe : c'est certainement dans le code d'untel !

Les pistes sont explorées une à une, parfois plusieurs fois. Il est 23h et la version livrable doit l'être absolument demain à 10h. Les machines de compilation sont silencieuses, elles attendent l'archivage salvateur pour démarrer leur production.

Et soudain, ça y est ! Bidule à trouvé. Bidule a corrigé un bug et le crash n'apparaît plus. Enfin, il est possible qu'il n'apparaisse plus car, forcément, la nature de celui-ci étant aléatoire, le doute subsiste. Mais la correction semble un bon candidat.

L'erreur ? Oh, une broutille. Une méthode qui, dans le cas d'un paramètre nul et pour éviter une division par zéro n'effectue pas la division et laisse le résultat indéterminé 1.

Le bug est-il vraiment corrigé ? Peut-être, peut-être pas. Peut-être était-ce un effet de bord. Les plus sceptiques vont se coucher avec un doute subsistant. Est-ce que ce module là a bien été testé ? Oui, machin l'a faut. Enfin je crois. Ah ! Pourvu que la version tienne !


Dans cette petite histoire, vécue par de nombreux programmeurs, le premier problème est le doute, le manque de confiance. Est-ce que les collaborateurs ont bien fait leur boulot ? Est-ce que le programme fait ce qu'on lui demande ? Est-ce que la correction de bug est la bonne ?

J'avais déjà parlé précédemment d'une manière de passer des contrats avec votre programme (les assertions). C'est une manière d'être certain qu'au moment de l'exécution, certaines conditions sont respectées. Les assertions statiques peuvent aussi vérifier certaines conditions au moment de la compilation.

Les assertions sont intéressantes et utiles. Mais elles présentent un défaut : elles ne vérifient la condition que si le code est exécuté et il n'est pas dit qu'elles ne se déclenchent pas dans certaines situations. C'est même ce qu'on attend d'elle : leur déclenchement en cas de situations malvenues.

C'est un premier pas dans la confiance dans son code : en passant un contrat, on écarte des cas.

Mais est-ce qu'on ne pourrait pas aller un cran plus loin et s'assurer que tout le code est exécuté à un moment où à un autre ? On peut imaginer un programme qui s'assure que tous les cas du programme à tester sont exécutés. L'ennui est que les programmes sont généralement complexes et les entrées innombrables.

Tests Unitaires

Une idée est alors de considérer qu'un programme auquel on peut faire confiance est avant tout un programme basé sur des composants auxquels ont peut faire confiance.

C'est ici qu'interviennent les tests unitaires. Les tests unitaires, comme leur nom l'indique, sont des tests qui testent une chose à la fois. En couvrant chaque « chose » du programme indépendamment en s'assurant que les sorties sont conformes à ce à quoi on s'attend en fonction des entrées, on a une bonne base de confiance.

Besoin d'un exemple ? Prenons une classe qui maintient une liste de valeurs et est capable de donner certaines statistiques sur ces valeurs. La valeur maximum et la moyenne par exemple.

Les tests unitaires couvrant cette classe pourraient vérifier ces choses 2 :

  • à la création, la méthode getNumberOfValue() renvoie 0 ;
  • à la création, la méthode getAverage() provoque une exception ;
  • à la création, la méthode getMax() renvoie la valeur minimale possible pour le domaine des valeurs ;
  • à la création, après avoir ajouté une valeur, getNumberOfValue() renvoie 1 ;
  • à la création, après avoir ajouté une valeur, getMax() renvoie cette valeur ;
  • à la création, après avoir ajouté une valeur, getAverage() renvoie cette valeur ;

Etc...

Ici, certains prennent peur. N'y a-t-il pas plus de code à écrire pour tester que dans la classe elle-même ? Oui. Dans ce cas exemple, c'est vrai. Et c'est vrai aussi dans la pratique dans un certain nombre de cas. Mais est-ce un mal ? Quelqu'un sans gros passif peut le penser. Quelqu'un qui a passé des nuits entières sur un bug idiot comprend vite que le temps passé à écrire les tests est gagné sur du temps qu'on ne passe pas plus tard à la recherche des bugs.

Ce n'est pas forcément évident à accepter car le temps qui n'est pas passé dans le debug est difficilement quantifiable.

Avec quoi tester ?

Il existe des frameworks de tests unitaires dans à peu près tous les langages de programmation. Pour le C++, vous pouvez en trouver quelques-uns que j'avais testé dans cet article. Pour Java, le plus connu est JUnit. Pour C#, NUnit.

Les frameworks sont généralement dotés de fonctions qui vérifient des résultats et d'une manière de signaler que du code est du code de test. Lorsque les tests sont lancés, chaque test est exécuté et une erreur lors d'une vérification provoque un message contenant des informations de ligne et un message indiquant l'erreur.

Par exemple, une erreur pourrait ressembler à cela :

testclass.cpp: 130 ; Test failed \"TestGetMax\" , expected 3 but got 1.

Tester pour développer

Tester a posteriori est intéressant. Et si l'on testait avant d'implémentation ? Idée étrange de prime abord, c'est une étape supplémentaire pas bête du tout. En anglais, le « test driven development » consiste donc à commencer par écrire le code de test avant ce que l'on test.

Forcément, lors des premiers lancements de tests, il y aura beaucoup d'erreurs. Mais pourvu que les tests soient bien écrits, on s'assure que lorsque tous les tests passent, l'implémentation testée fait ce qui lui a été demandée.

Un savoir faire

Tester ses implémentation ou encore tester avant d'implémenter ne sont pas des choses naturelles en programmation lorsque l'on débute. Ce ne sont pas non plus des choses, à ce que j'en sais, qui sont enseignées ou tout du moins appliquées après avoir été enseignées.

La plupart de temps, il faut avoir été confronté aux problèmes que ces méthodes règlent (ou tout du moins adoucissent) pour comprendre leur intérêt.

C'est aussi un savoir faire. Quoi tester, comment tester, comment bien isoler la classe à tester dans un langage objet, tester efficacement et complètement,... Mais c'est un outil qui se révélera utile. En d'autres mots : ça vaut le coup de s'y lancer.

Un graal ?

Je n'ai pas mis les tests unitaires et le « test driven development » (TDD) dans ma série « Ceci n'est pas le Graal ». Je ne pense pas que le TDD ait suivi le processus par lequel je définis un faux graal. Peut-être parce que ce n'est pas tant utilisé que ça. Peut-être parce que les utilisateurs de TDD sont convaincus et n'ont pas été forcés d'en faire. Peut-être parce qu'il n'y a pas eu un énorme buzz autour de cette méthode de développement.

Ou peut-être parce que la promesse est tenue et n'est pas sur-évaluée. Le test permet une chose : une confiance accrue dans le code. C'est le sujet de l'article. En surveillant qu'une modification ne provoque pas des régressions. En s'assurant que ce qui a été programmé fait bien ce à quoi on s'attend.

Cette confiance permet même d'oser plus. Qui n'a pas en tête le moment où un sous-système d'un programme est arrivé à un point où l'on voudrait le changer, mais où l'on n'ose pas, de peur de tout casser. Ou alors il faudra tester manuellement, mais sans vraiment être sûr de tous les cas. Ou alors il sera temps d'ajouter des tests unitaires, mais les faire a posteriori et bien après l'implémentation entraîne le risque d'oublier des cas. Avoir développé en TDD assure que les tests sont déjà là lorsqu'on en a besoin.

Le TDD est vraiment quelque chose que je conseille d'essayer. Je conseille aussi de commencer petit, avec des petites classes ou fonction que l'on a déjà implémentée, que l'on connaît bien et donc pour lesquelles on a une bonne idée de ce qu'il y a à tester. Puis d'aller un peu plus loin à chaque fois.


  1. ce cas est assez classique pour être neutre dans cet article. Il aurait pu s'agir d'un vecteur non normalisé, d'un dépassement de tableau, d'un pointeur fou,... 

  2. ce que l'on vérifie dépend du design de la classe.