Mouviciel

Création d'applications pour Mac OS X

Les coulisses d'un développeur Mac

Une petite introduction au test de logiciel

Publié le dimanche 4 janvier 2009, aucun commentaire

Je l'avais évoqué dans mon premier article sur le contrôle de version, le test de logiciel fait partie des outils indispensables du développeur. C'est pourtant l'un des plus mal connus, des plus mal enseignés, et je dirais même l'un de ceux qui ont la plus mauvaise réputation parmi les développeurs.

Qu'est-ce que le test de logiciel ?

Pour commencer, je vous propose une petite question : Comment définiriez-vous le test de logiciel ? La réponse la plus courante est quelque chose du genre : Le test est le processus qui permet de vérifier que le logiciel réalise correctement sa fonction. Tel que je le présente, vous vous doutez que ce n'est pas la bonne réponse. Voici pourquoi.

Imaginez-vous dans la peau d'un testeur à qui on confie un logiciel à tester. Selon la définition du test ci-dessus, votre objectif est de vérifier que ce logiciel est correct. Vous passez donc en revue chaque fonction élémentaire du logiciel. Vous écrivez un test pour chacune qui montre qu'elle se comporte correctement. Consciemment ou non, délibérément ou non, vous choisissez vos données de test de façon à attendre votre objectif : vérifier qu'il n'y a pas d'erreur. Vos données ont alors une faible probabilité de provoquer une défaillance du logiciel.

Maintenant, je vais vous proposer une autre définition du test et je vous invite à répondre en toute honnêteté à la question suivante : Avec la définition ci-dessous, est-ce que vous choisiriez vos entrées de test de la même manière ? Voici :

Le test de logiciel est le processus qui, par l'exécution du programme, permet de découvrir des erreurs.

À première vue, il ne s'agit que d'une subtilité sémantique : au lieu de vérifier que le logiciel est correct, on s'attache à y chercher des erreurs. Cependant, se donner pour objectif de faire échouer le programme plutôt que de le faire réussir a un effet psychologique très important. Cela induit un état d'esprit très différent : Le testeur cherche à casser le programme. Pour lui, un test qui réussit est un test qui a trouvé une erreur et un test qui échoue n'a pas su trouver d'erreur.

Le deuxième point fondamental du test de logiciel, celui-ci est plus connu, est qu'il est impossible de démontrer que toutes les erreurs d'un programme ont été trouvées. Quel que soit l'effort consacré au test, personne ne peut garantir que cet effort est suffisant. Le test exhaustif de toutes les entrées possibles d'un programme est impossible, même pour un programme simple.

Imaginez par exemple un logiciel dont la fonction est de multiplier un nombre par deux. Un test exhaustif devrait d'abord essayer tous les nombres possibles. Il y en a une infinité. Supposons que nous sachions que le programme est limité aux nombres entiers dont la représentation binaire tient sur 32 bits. Il y en a plus de quatre milliards. Après avoir épuisé tous ces nombres, le test exhaustif devrait s'intéresser à tous les autres nombres pour bien vérifier que le programme produit une erreur. Puis ce serait au tour de tous les caractères et chaînes de caractères : je veux m'assurer que le programme ne répond pas 14 à l'opération A*2, ni 32 à SEIZE*2. Je vous laisse imaginer un test exhaustif du compilateur gcc, pour lequel il faudrait essayer non seulement tous les fichiers sources corrects mais aussi tous les fichiers sources incorrects...

Ainsi pour minimiser l'effort, l'objectif du test de logiciel ne peut-être que de trouver le plus d'erreurs possibles avec le moins de tests possibles, autrement dit d'avoir des tests efficaces et d'éviter les tests qui ont peu de chances de trouver des erreurs. Cela signifie que le test de logiciel demande un certain nombre de techniques et une bonne dose de créativité. Un bon testeur est un artiste dans son domaine.

Couverture du test et critères de test

Une notion dont les développeurs parlent souvent à propos du test est celle de couverture : Est-ce que je dois atteindre une couverture de 100% ? Est-ce que cet objectif est simplement réaliste ? Comment faire ? ...

Le plus couramment, les éléments qu'on cherche à couvrir sont les instructions du code. On essaie de s'assurer que toutes les branches then et toutes les branches else, toutes les alternatives case d'un switch, tous les corps de boucles while ou for, ... sont exécutés lors d'une campagne de test.

La première question qui se pose est : Est-ce que cet objectif est atteignable ? Ma réponse à cette question est oui. À condition de se donner les moyens. Si je me suis donné la peine d'écrire une portion de code pour gérer une certaine situation, alors je dois reproduire cette situation pour vérifier que mon code se comporte correctement. Cela peut exiger un gros effort de mise en place de l'environnement de test, par exemple la création d'une base de données complexe ou bien d'appareils divers simulant le contexte du logiciel et d'autres observant son comportement. J'ai deux remarques pour tempérer mon affirmation. D'abord, tout cela n'est pas toujours simple. Il se peut même que le jeu n'en vaille pas la chandelle si par exemple le coût du test est très supérieur au coût occasionné par une défaillance du logiciel. Une option possible est alors de pratiquer une vérification statique du code par inspection. Ensuite il est parfois possible d'identifier des parties du code qui ne sont jamais atteignables : il s'agit de code mort, qu'on pourra alors choisir de supprimer.

La deuxième question qui se pose est : Est ce que cette couverture est suffisante ? Ma réponse à cette question est non. Supposons par exemple que j'ai un code avec une boucle for. Ce code est couvert à 100% dès que j'ai un test qui passe une fois dans la boucle. Les bugs étant ce qu'ils sont, il est très possible qu'il y en ait un qui ne soit activé qu'à la septième itération. Pour pouvoir être complet, je devrais couvrir non seulement chaque instruction du code mais aussi chaque chemin possible dans le code. Un cas de test couvrirait par exemple le chemin suivant : sept itérations dans cette boucle for suivi du else de cette condition puis quatre itérations là, etc. Bien entendu le test de tous les chemins d'un programme est irréalisable de la même façon que le test de toutes ses entrées. Pire que cela, le test de tous les chemins ne couvre que les chemins qui existent : comment couvrir le cas où le développeur a oublié un chemin ?

On le voit, l'art du testeur est de choisir les cas de test qui feront échouer le programme le plus efficacement. Dans le cas du test des chemins, il peut s'agir de ne s'attacher qu'à certains chemins : un classique est l'ensemble des chemins qui existent entre la déclaration d'une variable et l'utilisation de sa valeur.

On voit aussi se dessiner différents objectifs pour le test, on parle de critères de test. Il s'agit de définir les cas de test selon les éléments du logiciel qu'on souhaite exercer. On a déjà évoqué :

On peut ajouter à cette liste non exhaustive :

Ainsi, le testeur a tout un ensemble de flèches à son arc qui lui permettent d'atteindre son but selon des points de vues multiples. Bien entendu, la théorie du test est un domaine bien plus vaste que ce que je viens de vous brosser. Il y a cependant un dernier aspect que je voudrais évoquer avec vous. Il s'agit du développement guidé par les tests.

TDD : le développement guidé par les tests

TDD est l'acronyme de l'anglais Test Driven Development (ou Design). En français cela donne quelque chose comme développement (ou conception) guidé (ou dirigé) par les tests. C'est une technique beaucoup pratiquée dans les méthodes agiles de développement, en particulier l'extreme programming.

En pratique, le critère de test est le besoin de l'utilisateur décliné en un certain nombre d'histoires qui décrivent chacune une fonctionnalité du logiciel. À chaque histoire correspond un cas de test.

La particularité du TDD est que le cas de test est écrit d'abord, c'est-à-dire avant le code qui réalise la fonctionnalité. Il y a deux idées derrière cela. La première est qu'en écrivant le test, le développeur crée un scénario qui utilise la fonctionnalité. Il a donc besoin d'avoir les idées claires sur ce qu'il attend de la fonctionnalité en tant qu'utilisateur, sinon il demande des précisions à son client. La deuxième idée est la suivante. Lorsque le test est écrit, il est exécuté. Normalement, il doit provoquer une défaillance du logiciel puisque la fonctionnalité n'est pas encore écrite. Il s'agit ici de s'assurer que le test est bien capable de provoquer une défaillance. En d'autres termes, le développeur vérifie que son test n'est pas une variante de printf("OK\n");.

Ensuite, le développeur écrit la fonctionnalité en codant juste ce qu'il faut pour que le test ne provoque plus de défaillance. Il ne doit pas ajouter de complexité dans le but de pouvoir étendre la fonctionnalité plus tard, il doit aller au plus simple.

Ce n'est que plus tard, lorsque son client lui demandera une extension ou une autre fonctionnalité susceptible de modifier le code de la première, qu'il pourra changer son code initial. À ce moment-là, le test qu'il a écrit lui servira à vérifier que ses changements n'introduisent pas de régression. La capacité à ce test de non régression est un apport très important du TDD. En offrant au développeur un moyen de s'assurer que ses modifications n'ont pas d'impact sur l'existant, elle lui permet d'améliorer sans cesse le code sans crainte de tout casser.

Le leitmotiv du TDD est ainsi tester, coder, améliorer. Enfin, pour un maximum d'efficacité, il est nécessaire que l'exécution des tests soit entièrement automatique, voire intégrée à la phase de compilation. Ainsi cette confiance des développeurs dans le code qu'ils ont produit est constamment renouvelée.

Voilà donc un petit aperçu du test de logiciel, dans lequel je vous ai proposé une définition du test, quelques éléments pour guider le choix des tests et une démarche de test qui permet de mieux maîtriser le besoin, le développement et la communication entre client et développeur. Comment mettre tout cela en pratique dans Xcode est une autre histoire que je vous raconterai peut-être un jour prochain...

Commentaires

Soyez le premier à commenter cet article.

Ajoutez un commentaire

Les commentaires sont destinés à répondre en public à l'article visé et à ses commentaires déjà publiés. C'est pourquoi seul un pseudonyme vous est demandé ici. Si vous souhaitez échanger avec moi en privé, merci de me laisser un message via le menu contact en haut de la page.

Archive

Rubriques

Abonnement