Instructions: Idées de test pour les appels de méthode
Les idées de test se fondent sur des fautes plausibles d'un logiciel et sur la meilleure façon de les découvrir. Ces instructions décrivent une méthode de détection de situations dans lesquelles le code ne gère pas le résultat de l'appel d'une méthode.
Relations
Eléments connexes
Description principale

Introduction

Voici un exemple de code incorrect :

File file = new File(stringName);
file.delete();

Le problème est que File.delete peut échouer mais que le code ne fait pas de vérification pour cette erreur. La solution consiste à ajouter le code en italique montré ici :

File file = new File(stringName);



if (file.delete()


== false) {...}

Ces instructions décrivent une méthode de détection de situations dans lesquelles le code ne gère pas le résultat de l'appel d'une méthode. Elles supposent que la méthode appelée entraîne un résultat correct, quelle que soit l'entrée donnée. Ce point doit faire l'objet d'un test, mais la création d'idées de test pour la méthode appelée est une tâche à part. En d'autres termes, vous n'avez pas à vous charger de tester File.delete.)

La notion clé est que vous devez créer une idée de test pour chaque résultat pertinent non géré de l'appel de la méthode. Pour définir ce terme, commençons par le résultat. Lors de son exécution, une méthode change l'état de la réalité. Exemples :

  • Elle peut placer des valeurs de retour sur la pile d'exécution.
  • Elle peut lancer une exception.
  • Elle peut modifier une variable globale.
  • Elle peut mettre à jour un enregistrement dans une base de données.
  • Elle peut envoyer des données sur le réseau.
  • Elle peut imprimer un message dans la sortie standard.

Observons maintenant l'aspect pertinent du résultat, là encore à l'aide de quelques exemples.

  • Imaginez que la méthode appelée appelle un message dans la sortie standard. Cette action change l'état de la réalité mais n'affecte pas le traitement à venir de ce programme. Quel que soit le contenu imprimé et même si l'impression est vide, l'exécution du code ne peut être affectée.
  • Si la méthode renvoie true pour indiquer qu'elle a abouti et false en cas d'échec, il est fort probable que votre programme tienne compte du résultat. Cette valeur de retour est donc pertinente.
  • Si la méthode appelée met à jour un enregistrement de base de données que votre code lit et utilise par la suite, le résultat (mise à jour) est pertinent.

Il n'existe pas de limite absolue entre pertinent et non pertinent. En appelant print, votre méthode peut entraîner l'allocation éventuellement pertinente des mémoires tampons. Il est possible qu'un incident dépende de l'opération d'allocation et des mémoires tampons allouées. Possible certes, mais est-ce pour autant plausible ?

Une méthode peut souvent avoir un très grand nombre de résultats, dont certains seulement sont différents. Imaginez par exemple une méthode écrivant des octets sur le disque. Elle peut renvoyer un nombre inférieur à zéro pour signaler un échec ; sinon, elle retourne le nombre d'octets écrits, qui peut être inférieur au nombre demandé. Les diverses possibilités peuvent être rassemblées dans trois résultats distincts :

  • le nombre est inférieur à zéro,
  • le nombre écrit est égal à celui demandé,
  • des octets ont été écrits, mais moins que le nombre demandé.

Toutes les valeurs inférieures à zéro sont regroupées dans un résultat, car aucun programme ne fera la différence entre elles. Toutes les valeurs (dans le cas où, justement, il est possible d'en avoir plusieurs) doivent être traitées comme des erreurs. De la même façon, si le code demandait l'écriture de 500 octets, peu importe si seuls 34 ou 340 ont vraiment été écrits ; il en sera peut-être de même avec les octets non écrits. Si une valeur doit être traitée différemment (0, par exemple), elle entraîne un nouveau résultat.

Il reste un dernier point pour la définition. Cette technique de test ne s'intéresse pas aux différents résultats déjà gérés. Prenez le code suivant :

File file = new File(stringName);
if (file.delete() == false) {...}

Il existe deux résultats distincts (true et false) que le code gère. La gestion peut être incorrecte, mais les idées de test issues de Instructions pour le produit : Idées de test pour des expressions booléennes et des conditions frontière le vérifieront. Cette technique de test s'intéresse aux différents résultats qui ne sont pas précisément gérés par du code distinct. Cette situation peut se produire pour deux raisons : vous avez pensé que la différence n'était pas importante ou elle vous a échappé. Voici un exemple du premier cas de figure :

result = m.method();
switch (result) {
    case FAIL:
    case CRASH:
       ...
       break;
    case DEFER:
       ...
       break;
    default:
       ...
       break;
}

FAIL CRASH sont gérés par le même code. Il peut être prudent de vérifier que ce principe est approprié. Voici maintenant un exemple du second cas de figure :

result = s.shutdown();
if (result == PANIC) {
   ...
} else {
   // Réussite. Eteindre le réacteur.
   ...
} 

Il s'avère que l'arrêt peut renvoyer un résultat distinct : RETRY. Le code tel qu'écrit traite cette situation comme dans un cas de réussite, ce qui a de fortes chances d'être incorrect.

Recherche d'idées de test

Votre objectif est de considérer les résultats importants qui vous ont échappé auparavant, ce qui semble impossible : pourquoi penseriez-vous maintenant qu'ils sont importants si vous ne l'avez pas fait plus tôt ?

Réponse : une révision systématique de votre code, avec une mentalité de test au lieu d'une approche de programmation, peut parfois vous offrir de nouveaux points de vue. Vous pouvez remettre en cause vos propres hypothèses en parcourant votre code de façon méthodique, en observant les méthodes appelées, en repassant leur documentation et en réfléchissant. Voici quelques situations à rechercher.

Cas "impossibles"

Il s'avère souvent que les codes erreur sont impossibles. Vérifiez à nouveau vos hypothèses.

Cet exemple montre l'implémentation Java d'un schéma de mise en oeuvre Unix pour gérer des fichiers temporaires.

File file = new File("tempfile");
FileOutputStream s;
try {
    // ouvrir le fichier temp.
    s = new FileOutputStream(file);
} catch (IOException e) {...}
// vérifier que le fichier temp sera supprimé
file.delete();

L'objectif est de vérifier qu'un fichier temporaire est toujours supprimé, quelle que soit la façon dont le programme est quitté. Pour ce faire, créez le fichier temporaire et supprimez-le immédiatement. Sous Unix, vous pouvez poursuivre votre travail avec le fichier supprimé et le système d'exploitation se charge du nettoyage au terme du processus. Un programmeur Unix recherchant la facilité n'écrira peut-être pas de code pour vérifier un échec de suppression. De la même façon que la création du fichier a abouti, sa suppression doit aussi réussir.

Cette astuce ne fonctionne pourtant pas dans Windows. La suppression échoue en effet car le fichier est ouvert. Identifier ce motif n'est pas simple : jusqu'à août 2000, la documentation Java ne mentionnait pas les situations dans lesquelles delete pouvait échouer et se contentait de signaler qu'un échec était possible. Cependant, dans un contexte de test, le programmeur peut mettre en cause cette hypothèse. Comme son code est censé être écrit une fois pour s'exécuter partout, il doit demander à un programmeur Windows dans quels cas File.delete échoue sous Windows et donc découvrir la triste réalité.

Cas "non pertinents"

Un autre facteur empêchant de remarquer une valeur pertinente est d'être déjà convaincu qu'elle est sans importance. Une méthode Java comparedes outils de comparaison renvoie un nombre <0, 0, ou >0. Il s'agit de trois cas distincts à essayer. Ce code réunit deux d'entre eux :

void allCheck(Comparator c) {
   ...
   if (c.compare(o1, o2) <= 0) {
      ...
   } else {
      ...
   } 

Ce code peut toutefois s'avérer incorrect. Pour découvrir si tel est le cas, essayez les deux situations séparément, même si vous êtes convaincu que le résultat sera identique. Vos convictions sont en fait ce que vous testez. Vous pouvez exécuter le cas then de l'instruction if à plusieurs reprises pour d'autres raisons. Pourquoi ne pas essayer une fois avec le résultat inférieur à 0 et une autre avec un résultat nul ?

Exceptions non interceptées

Les exceptions sont une sorte de résultat distinct. Prenez le code qui suit comme point de départ :

void process(Reader r) {
   ...
   try {
      ...
      int c = r.read();
      ...
   } catch (IOException e) {
      ...
   }
}

Vous êtes censé vérifier si le code gestionnaire agit comme il se doit en cas d'échec de lecture. Imaginez cependant qu'une exception est explicitement non gérée. A la place, elle est autorisée à se propager à travers le code testé. Dans Java, le résultat donnerait ce qui suit :

void process(Reader r) 


throws IOException {
    ...
    int c = r.read();
    ...
}

Cette technique vous demande de tester ce cas même si le code ne le gère pas de façon explicite. Pourquoi cela ? La raison est ce type de faute :

void process(Reader r) throws IOException {
    ...
    


Tracker.hold(this);
    ...
    int c = r.read();
    ...
    


Tracker.release(this);
    ...
}

Ici, le code affecte l'état général (via Tracker.hold). Si l'exception est lancée, Tracker.release n'est jamais appelé.

L'échec de publication n'aura certainement aucune conséquence immédiate. Le problème ne sera probablement pas visible tant que le processus n'est pas appelé à nouveau, d'où le nouvel échec de mise en attente de l'objet. L'article de Keith Stobie ("Testing for Exceptions") apporte des informations utiles sur ces incidents (en anglais).   (Télécharger Adobe Reader))

Fautes non découvertes

Cette technique particulière ne traite pas tous les incidents associés aux appels de méthodes. Deux types ont toutes les chances d'y échapper.

Arguments incorrects

Prenez les deux lignes de code C suivantes, la première étant incorrecte et la seconde correcte.

... strncmp(s1, s2, strlen(s1)) ...
... strncmp(s1, s2, strlen(


s2)) ...

strncmp compare deux chaînes et renvoie un nombre inférieur à 0 si la première est antérieure à la seconde sur le plan lexicographique (son entrée arrive avant dans un dictionnaire). Il renvoie la valeur 0 si elles sont égales. Le nombre retourné est en revanche supérieur à 0 si la première chaîne est plus grande sur le plan lexicographique. Toutefois, la comparaison porte uniquement sur le nombre de caractères indiqué par le troisième argument. Le problème est que la longueur de la première chaîne sert à limiter la comparaison, alors qu'il devrait en fait s'agir de celle de la seconde chaîne.

Cette technique demande trois tests, un pour chaque valeur de retour. En voici trois possibles :

s1 s2 résultat attendu résultat obtenu
"a" "bbb" <0 <0
"bbb" "a" >0 >0
"foo" "foo" =0 =0

L'incident n'est pas identifié car rien dans cette technique n'oblige le troisième argument à posséder une valeur déterminée. Un jeu d'essai comme suit est alors nécessaire :

s1 s2 résultat attendu résultat obtenu
"foo" "food" <0 =0

Même si certaines techniques se prêtent à la détection de ce genre d'incidents, elles sont dans la pratique rarement appliquées. Votre effort de test sera probablement plus profitable si vous le consacrez à un ensemble de tests visant de nombreux types d'incidents et pouvant éventuellement identifier cet incident comme un effet secondaire.

Résultats confus

Il existe un danger lié au fait de coder et de tester méthode par méthode. Voici un exemple avec deux méthodes. La première, connect, tente d'établir une connexion réseau :

void connect() {
   ...
   Integer portNumber = serverPortFromUser();
   if (portNumber == null) {
      // message contextuel sur le numéro de port invalide
      return;
   }

Lorsqu'elle a besoin d'un numéro de port, elle appelle serverPortFromUser. Cette méthode renvoie alors deux valeurs distinctes. Elle retourne un numéro de port choisi par l'utilisateur s'il est valide (1000 ou supérieur). Sinon, elle renvoie une valeur null. Dans ce cas, le code testé émet un message d'erreur et prend fin.

Lorsque la méthode connect est testée, elle fonctionne comme prévue : un numéro de port valide permet d'établir une connexion et un numéro incorrect entraîne un message d'erreur.

Le code pour serverPortFromUser est légèrement plus compliqué. Il ouvre d'abord une fenêtre demandant une chaîne et comportant les boutons OK et Annuler standard. En fonction de l'intervention de l'utilisateur, quatre scénarios sont possibles :

  1. Si l'utilisateur entre un nombre valide, ce nombre est renvoyé.
  2. Si le nombre est trop petit (inférieur à 1000), une valeur null est renvoyée et le message sur le numéro de port incorrect affiché.
  3. Si le format du nombre est incorrect, la valeur null est à nouveau renvoyée et le même message s'affiche également.
  4. Si l'utilisateur clique sur Annuler, la valeur null est renvoyée.

Ce code fonctionne aussi comme prévu.

La combinaison des deux extraits de code a cependant une conséquence négative : l'utilisateur clique sur Annuler et obtient un message au sujet d'un numéro de port erroné. L'ensemble du code fonctionne comme prévu mais l'effet global est toujours incorrect. Il a été testé de façon appropriée, sauf qu'un incident n'a pas été identifié.

Le problème ici est que la valeur null est l'un des résultats possédant deux significations (valeur incorrecte ou annulé par l'utilisateur). Rien dans cette technique ne vous fait remarquer ce défaut de conception de serverPortFromUser.

Un test peut cependant aider. Lorsque vous testez serverPortFromUser de façon isolée pour voir si la valeur renvoyée est celle prévue dans chacun de ces quatre cas, le contexte d'utilisation est perdu. Imaginez à la place un test avec la méthode connect. Dans ce cas, quatre tests appliqueraient les deux méthodes à la fois :

entrée résultat attendu processus imaginé
l'utilisateur entre "1000" la connexion au port 1000 est établie serverPortFromUser renvoie un nombre qui est utilisé

l'utilisateur entre "999"

message d'erreur à propos d'un numéro de port incorrect

serverPortFromUser retourne une valeur null qui affiche un message

l'utilisateur entre "i99"

message d'erreur à propos d'un numéro de port incorrect serverPortFromUser retourne une valeur null qui affiche un message
l'utilisateur clique sur Annuler tout le processus de connexion doit être annulé serverPortFromUser renvoie une valeur null, ce qui n'a pas de sens

Comme c'est souvent le cas, le test dans un contexte plus large dévoile des problèmes d'intégration échappant aux tests de portée plus réduite. Comme bien souvent aussi, une réflexion poussée lors de la conception du test permet d'anticiper le problème avant d'exécuter le test. Si l'incident n'est toutefois pas détecté avant, il le sera lors du test.