Rubriques

Introduction To top of page

L'expression "Test développeur" sert à désigner une catégorie d'activités de test réalisées de préférence par les développeurs du logiciel. Cela comprend également les artefacts créés pour ces activités. Les tests développeur comprennent le travail traditionnellement considéré dans les catégories suivantes : tests d'unité, beaucoup de tests d'intégration, et certains aspects de ce que l'on appelle souvent tests système. Bien que les tests développeurs soient traditionnellement associés aux activités de la discipline Implémentation, ils ont également un rapport avec les activités des disciplines Analyse et Conception.

En envisageant les tests développeur de cette façon "holistique", vous contribuez à atténuer certains risques associés à l'approche plus "atomiste" généralement adoptée. Dans l'approche traditionnelle des tests développeur, l'effort se concentre au départ sur l'évaluation que toutes les unités fonctionnent de façon indépendante. Plus tard dans le cycle de vie du développement, alors que le travail de développement tire à sa fin, les unités intégrées sont assemblées dans un sous-système de travail et testées pour la première fois dans cette configuration.

Cette approche présente un certain nombre de défauts. Tout d'abord, parce qu'elle encourage une approche par étapes des tests des unités intégrées ou des sous-systèmes par la suite ; les erreurs identifiées pendant ces tests sont souvent trouvées trop tard. Leur découverte tardive entraîne généralement la décision de ne prendre aucune mesure corrective, ou bien la correction de ces erreurs exige un travail supplémentaire considérable. Ce travail supplémentaire coûte cher et empêche d'avancer dans d'autres domaines. Cela augmente le risque que le projet déraille ou soit abandonné.

Deuxièmement, créer de frontières rigides entre les tests unitaires, d'intégration et système augmente la probabilité que personne ne découvre les erreurs traversant les frontières. Le risque est complexe lorsque la responsabilité de ces types de tests est attribuée à des équipes séparées.

Le style de tests développeurs recommandés par RUP encourage le développeur à se concentrer sur les tests les plus importants et appropriés à réaliser à un moment donné. Même dans la portée d'une unique itération, il est généralement plus efficace pour le développeur de trouver et corriger autant d'anomalies que possible dans son propre code, sans les coûts indirects supplémentaire qu'implique le transfert à un groupe de tests séparé. Le résultat souhaité est la découverte précoce de la plupart des erreurs logicielles les plus importantes ; que ces erreurs soient ou non dans des unités indépendantes, l'intégration des unités ou le fonctionnement des unités intégrées dans un scénario d'utilisateur final significatif.

Pièges du démarrage des tests développeur To top of page

Beaucoup de développeurs qui essaient de faire un travail de test considérablement plus approfondi abandonnent souvent peu de temps après avoir commencé. Ils estiment que cela ne semble rien apporter. En outre, certains développeurs qui commencent bien les tests développeur trouvent qu'ils ont créé une suite de tests impossibles à maintenir qu'ils finissent par abandonner.

Cette page vous fournit quelques conseils pour franchir les premiers obstacles et pour créer une suite de tests évitant le piège de maintenabilité. Pour plus d'informations, voir Principes et conseils : Maintenir des suites de tests automatisés.

Définir les attentes Haut de la page

Ceux qui trouvent les tests développeur gratifiant le font. Ceux qui les considèrent comme une corvée trouvent le moyen d'y échapper. Cela est simplement dans la nature de la plupart des développeurs dans la plupart des secteurs, et traiter cela comme un manque honteux de discipline n'a jamais donné de résultats. Ainsi, en tant que développeur, vous devez attendre des tests qu'ils soient gratifiants et faire ce qu'il faut pour les rendre gratifiants.

Les tests développeur idéaux suivent une boucle éditer-tester très serrée. Vous apportez un petit changement au produit, comme ajouter une nouvelle méthode à une classe, puis vous exécutez le test à nouveau immédiatement. Si un test se casse, vous savez exactement quel code en est la cause. Ce rythme simple et régulier de développement est ce qui est le plus gratifiant dans les tests développeur. Les longues sessions de débogage doivent être exceptionnelles.

Etant donné qu'il n'est pas rare qu'un changement apporté dans une classe casse quelque chose dans une autre, vous devez prévoir d'exécuter à nouveau non seulement les tests de la classe changée mais de nombreux autres. Dans l'idéal, vous exécutez à nouveau la suite de tests complète pour votre composant plusieurs fois par heure. Chaque fois que vous apportez un changement important, vous exécuter de nouveau la suite, étudiez les résultats et soit procédez au changement suivant soit corrigez le dernier changement. Prévoyez de faire des efforts pour rendre cette remontée d'informations aussi rapide que possible.

Automatiser vos tests To top of page

Exécuter des tests souvent n'est pas pratique si les tests sont manuels. Pour certains composants, les tests automatisés sont faciles. Un exemple serait une base de données intégrée à la mémoire. Elle communique avec ses clients à travers une API et ne possède aucune autre interface vers l'extérieur. Les tests pour cela ressembleraient à ce qui suit :

/* Check that elements can be added at most once. */
// Setup
Database db = new Database();
db.add("key1", "value1");
// Test
boolean result = db.add("key1", "another value");
expect(result == false);

Seule une chose différencie ces tests du code client ordinaire : au lieu de croire dans les résultats des appels API, ils vérifient. Si l'API facilite l'écriture du code client, elle facilite l'écriture du code de test. Si le code de test n'est pas facile à écrire, vous avez reçu au début l'avertissement que l'API pouvaient être améliorée. La Conception pilotée par le test est ainsi cohérente avec l'approche de Rational Unified Process de se concentrer sur la prise en considération précoce des risques importants.

Cependant, plus le composant est étroitement lié au monde extérieur, plus il sera difficile à tester. Il y a deux cas courants : les interfaces utilisateur graphiques et les composants de back-end.

Interfaces utilisateur graphiques

Supposons que la base de données de l'exemple ci-dessus reçoive ses données via une procédure de rappel d'un objet interface utilisateur. La procédure de rappel est appelée lorsque l'utilisateur remplit certaines zones de texte et appuie sur un bouton. Vous n'avez certainement pas envie de tester cela en remplissant manuellement les zones et en appuyant sur le bouton plusieurs fois par heure. Vous devez trouver un moyen de fournir l'entrée sous une commande programmée, généralement en "appuyant" le bouton en code.

Appuyer sur le bouton entraîne l'exécution d'un code dans le composant. Il est très probable que ce code change l'état de certains objets de l'interface utilisateur. Vous devez donc prévoir également une façon d'interroger ces objets au moyen d'un programme.

Composants de Back-end

Supposons que le composant testé n'implémente pas une base de données. A la place, c'est une classe enveloppante autour d'une basse de données réelle sur disque. Les tests par rapport à cette base de données réelle peuvent être difficiles. Elle peut être difficile à installer et configurer. Ses licences peuvent coûter cher. La base de données peut suffisamment ralentir les tests pour vous décourager de les exécuter souvent. Dans certains cas, il vaut la peine de "tronçonner" la base de données avec un composant plus simple qui fait le strict nécessaire pour prendre les tests en charge.

Les tronçons sont également utiles quand un composant avec lequel votre composant communique n'est pas encore prêt. Vous ne voulez pas que vos tests dépendent du code de quelqu'un d'autre.

Pour plus d'informations, voir Concepts: Tronçons.

Ne pas écrire ses propres outils Haut de lapage

Les tests développeur semblent assez simples. Vous définissez des objets, vous effectuez un appel à travers une API, vous vérifiez le résultat, et vous annoncez un échec de test si les résultats ne correspondent pas à vos attentes. Il est également commode de disposer d'un moyen de grouper les tests afin de pouvoir les exécuter individuellement ou sous forme de suites complètes. Les outils qui soutiennent ces exigences s'appellent cadres de test.

Les tests développeur sont simples et les exigences des cadres de test ne sont pas compliquées. Toutefois, si vous succombez à la tentation d'écrire votre propre cadre de test, vous passerez davantage de temps à bricoler le cadre que vous vous y attendez. Plusieurs cadres de test sont disponibles, à acheter ou gratuits, et il n'y a pas de raison de ne pas les utiliser.

Créer un code de soutien To top of page

Le code de test a tendance à être répétitif. Il est courant de voir des séquences de code comme celle-ci :

// null name not allowed
retval = o.createName(""); 
expect(retval == null);
// leading spaces not allowed
retval = o.createName(" l"); 
expect(retval == null);
// trailing spaces not allowed
retval = o.createName("name "); 
expect(retval == null);
// first character may not be numeric
retval = o.createName("5allpha"); 
expect(retval == null);

Ce code est créé en copiant une vérification, en la collant, puis en l'éditant pour faire une autre vérification.

Le danger est ici double. Si l'interface change, il y aura un gros travail d'édition. (Dans des cas plus compliqués, un simple remplacement global ne suffira pas.) De plus, si le code est très compliqué, l'objet du test peut se perdre au milieu du texte.

Lorsque vous trouvez que vous vous répétez, envisagez sérieusement d'automatiser la répétition en code de soutien. Même si le code ci-dessus est un exemple simple, il est plus lisible et facile à entretenir s'il est écrit comme ceci :

void expectNameRejected(MyClass o, String s) {
    Object retval = o.createName(s);
    expect(retval == null);
}
...
// null name not allowed
expectNameRejected(o, ""); 
// leading spaces not allowed.
expectNameRejected(o, " l"); 
// trailing spaces not allowed.
expectNameRejected(o, "name "); 
// first character may not be numeric.
expectNameRejected(o, "5alpha"); 

Les développeurs qui rédigent des tests ont souvent tendance à recourir de trop au copier-coller. Si vous avez cette tendance, il est utile de s'orienter volontairement dans l'autre direction. Résoudre cela supprimera tout le texte en double de votre code.

Commencer par écrire les tests To top of page

Ecrire les tests après le code est une corvée. Le travail est souvent bâclé pour terminer au plus vite et passer à la suite. Ecrire les tests avant le code intègre les tests dans une boucle de remontée d'informations positive. Au fur et à mesure que vous implémentez davantage de codes, davantage de tests réussiront jusqu'à ce que finalement tous les tests réussissent et que le travail soit terminé. Les personnes qui commencent par rédiger les tests semblent mieux réussir et ne mettent pas plus de temps. Pour en savoir plus sur l'importance des tests, voir Concepts: Conception pilotée par le test

Maintenir les tests compréhensibles Haut de lapage

Il faut vous attendre à ce que vous ayez, ou quelqu'un d'autre, à modifier les tests ultérieurement. Une situation type est qu'une itération ultérieure appelle un changement dans le comportement du composant. En tant qu'exemple simple, supposons que le composant a une fois déclaré une méthode de racine carrée de cette façon :

double sqrt(double x);

Dans cette version, un argument négatif a obligé sqrt à revenir à NaN ("not a number" de la norme IEEE 754-1985 Standard for Binary Floating-Point Arithmetic). Dans la nouvelle itération, la méthode de racine carrée acceptera les nombres négatifs et produire un résultat complexe :

Complex sqrt(double x);

Les anciens tests pour sqrt devront changer. Cela implique de comprendre ce qu'ils font et de les mettre à jour afin qu'ils fonctionnent avec le nouveau sqrt. Lorsque vous mettez des tests à jour, vous devez veiller à ne pas détruire leur capacité à trouver les bogues. Il se passe parfois la chose suivante :

void testSQRT () {
	//  Update these tests for Complex 
	// when I have time -- bem
	/*
		double result = sqrt(0.0);
		...
	*/
}

D'autres moyens sont plus subtils : les tests sont changés afin de fonctionner réellement mais ils ne testent plus ce pour quoi ils ont été conçus à l'origine. Après un grand nombre d'itérations, on peut obtenir une suite de tests n'ayant pas la capacité de déceler de nombreux bogues. On appelle parfois cela "déclin de la suite de tests". Une suite en déclin sera abandonnée car elle ne faut plus la peine d'être maintenue.

Vous ne pouvez pas maintenir la capacité de trouver les bogues d'un test sans que les Idées de test qu'un test implémente ne soient claires. Le code de test a tendance à être accompagné que de peu de commentaires, même s'il est souvent plus difficile de comprendre le "pourquoi" derrière lui que le code produit.

Il est plus probable que le déclin de la suite de tests se produise dans les tests directs pour sqrt que dans les tests indirects. Il y aura un code qui appelle sqrt. Ce code aura des tests. Quand sqrt changera, certains de ces tests échoueront. La personne qui change sqrt devra probablement changer ces tests. Parce qu'elle les connaît moins, et parce que leur relation avec le changement est moins claire, elle court plus de risques de les affaiblir dans leur processus de réussite.

Lorsque vous créez un code de soutien pour les tests (comme vivement conseillé ci-dessus), soyez attentif : le code de soutien doit clarifier et non pas embrouiller l'objet des tests qui l'utilisent. On se plaint souvent que les programmes orientés objets n'ont aucun endroit où rien n'est fait. Si vous regardez une méthode, vous trouverez qu'elle transmet son travail autre part. Une telle structure présente des avantages, mais elle rend la compréhension du code plus difficile pour les nouvelles personnes. A moins qu'elles fassent un effort, leurs changements ont des chances d'être erronés ou le code risque d'être plus compliqué et fragile. Il en va de même pour le code de test, sauf que les anciens mainteneurs sont encore moins susceptibles de s'en occuper comme il se doit. Vous devez éviter le problème en rédigeant des tests compréhensibles.

Faire correspondre la structure du test à la structure du produit To top of page

Supposons que quelqu'un hérite de votre composant. Et qu'il a besoin d'en changer une partie. Il peut avoir besoin d'examiner les anciens tests pour l'aider dans sa nouvelle conception. Il souhaite mettre à jour l'ancien test avant d'écrire le code (Conception pilotée par le test).

Toutes ces bonnes intentions seront vaines s'il ne peut pas trouver les tests adéquats. Ce qu'il fera, c'est apporter le changement, voir si le test échoue et le corriger. Cela contribue au déclin de la suite de tests.

Pour cette raison, il est important que la suite de tests soit bien structurée et que l'emplacement des tests soit prévisible à partir de la structure du produit. Le plus couramment, les développeurs organisent les tests selon une hiérarchie parallèle, avec une classe de test par classe de produit. Ainsi, si quelqu'un change une classe nommée Log, il sait que la classe de test est TestLog, et il sait où trouver le fichier source.

Laisser les tests enfreindre l'encapsulation Haut de la page

Vous pouvez limiter vos tests pour interagir avec composant, exactement comme le code client, à travers la même interface que le code client utilise. Toutefois, cela présente des désavantages. Supposons que vous testez une classe simple qui maintient une liste doublement liée :

Image d'un exemple de liste à deux liens

Fig1 : liste à deux liens

Notamment, vous testez la méthode DoublyLinkedList.insertBefore(Object existing, Object newObject). Dans l'un de vos tests, vous voulez insérer un élément au milieu de la liste, puis vérifier s'il a été inséré avec succès. Le test utilise la liste ci-dessus pour créer cette liste mise à jour :

Image d'un exemple de liste à deux liens avec élément inséré

Fig2 : liste à deux liens - élément inséré

L'exactitude de la liste est vérifiée comme suit :

// the list is now one longer. 
expect(list.size()==3);
// the new element is in the correct position
expect(list.get(1)==m);
// check that other elements are still there.
expect(list.get(0)==a);
expect(list.get(2)==z);

Cela semble suffisant mais ne l'est pas. Supposons que l'implémentation de la liste est erronée et que les pointeurs de retour sont correctement réglés. C'est-à-dire, supposons que la liste mise à jour ressemble en fait à cela :

Image d'exemple de liste à deux liens avec erreur d'implémentation

Fig3 : Liste à deux liens - erreur d'implémentation

Si DoublyLinkedList.get(int index) traverse la liste du début à la fin (probable), le test ne verra pas cette anomalie. Si la classe fournit les méthodes elementBefore et elementAfter, la vérification de ces anomalies est facile :

// Check that links were all updated
expect(list.elementAfter(a)==m);
expect(list.elementAfter(m)==z);
expect(list.elementBefore(z)==m); //this will fail
expect(list.elementBefore(m)==a);

Mais que se passe-t-il s'elle ne fournit pas ces méthodes ? Vous pourriez concevoir des séquences plus élaborées d'appels de méthodes qui échoueront si l'anomalie suspectée est présente. Par exemple :

// Check whether back-link from Z is correct.
list.insertBefore(z, x);
// If it was incorrectly not updated, X will have 
// been inserted just after A.
expect(list.get(1)==m); 

Mais la création d'un tel test représente plus de travail et sera probablement plus difficile à maintenir. (A moins que vous n'écriviez de bons commentaires, personne ne comprendra pourquoi le test fait ce qu'il fait.) Il existe deux solutions :

  1. Ajouter les méthodes elementBefore et elementAfter à l'interface publique. Mais cela expose en réalité d'implémentation à tout le monde et rend les changements ultérieurs plus difficiles.
  2. Laisser les tests "regarder sous le capot" et vérifier directement les pointeurs.

Cette dernière solution est la meilleure, même pour une classe simple comme DoublyLinkedList et notamment pour les classes plus complexes qui se présentent dans vos produits.

Généralement, les tests sont mis dans le même package que la classe qu'ils testent. On leur attribue un accès protégé ou ami.

Erreurs caractéristiques de la conception des testsTo top of page

Chaque test exerce un composant et vérifie les résultats corrects. La conception du test (les intrants qu'il utilise et comment il vérifie l'exactitude) peut être bonne pour révéler les anomalies ou elle peut les masquer par inadvertance. Voici quelques caractéristiques d'erreurs de conception des tests.

La non spécification des résultats attendus à l'avance To top of page

Supposez que vous testez un composant qui convertit le format XML en HTML. Vous pouvez être tenté de prendre des fichiers XML, de les convertir et de regarder le résultat dans un navigateur. Si l'écran semble correct, vous "bénissez" le fichier HTML en l'enregistrant comme résultat attendu officiel. Ensuite, un test compare le véritable résultat de la conversion aux résultats attendus.

Ceci est une pratique dangereuse. Même les utilisateurs avertis d'ordinateurs ont l'habitude de croire ce que fait l'ordinateur. Il est probable que des erreurs dans l'apparence de l'écran vous échappent. (Sans mentionner le fait que les navigateurs sont assez tolérants aux fichiers HTML mal formatés.) En désignant ce fichier HTML erroné comme le résultat attendu officiel, vous garantissez que le test ne trouvera jamais le problème.

Il est moins dangereux de vérifier deux fois en regardant directement le fichier HTML mais cela reste toutefois dangereux. Parce que l'extrant est compliqué, il sera facile d'oublier des erreurs. Vous trouverez plus d'anomalies si vous commencez par écrire manuellement le résultat attendu.

La non vérification de l'arrière-plan To top of page

Les tests vérifient généralement que ce qui doit avoir été changé l'a été, mais leurs créateurs oublient souvent de vérifier que ce qui doit rester inchangé l'est resté. Par exemple, supposons qu'un programme est censé changer les 100 premiers enregistrements d'un fichier. C'est une bonne idée que de vérifier que le 101ème n'a pas été changé.

En théorie, vous vérifiez que rien dans l'"arrière-plan" (le système du fichier entier, toute la mémoire, tout ce qui est accessible à travers le réseau) n'est resté inchangé. Dans la pratique, vous devez choisir avec soin ce que vous pouvez vous permettre de vérifier. Mais il est important de faire ce choix.

La non-vérification de la persistance To top of page

Ce n'est pas parce qu'un composant vous dit qu'un changement a été fait qu'il a été réellement apporté dans la base de données. Vous devez vérifier la base de données d'une autre façon.

La monotonie To top of page

Un test peut être conçu pour vérifier l'effet de trois champs dans un enregistrement de base de données, mais beaucoup d'autres champs doivent être renseignés pour exécuter le test. Les testeurs utiliseront souvent les mêmes valeurs continuellement pour ces champs "sans importance". Par exemple, ils utiliseront toujours le nom de leur fiancée dans une zone de texte ou 999 dans une zone numérique.

Le problème est que parfois ce qui ne devrait pas avoir d'importance en a dans la réalité. Parfois, il y a un bogue qui dépend d'une combinaison obscure d'entrées improbables. Si vous utilisez toujours les mêmes entrées, vous n'avez aucune chance de trouver ces bogues. Si vous changez les entrées en permanence, vous pourrez peut-être les découvrir. Assez souvent, il ne coûte presque rien d'utiliser un chiffre différent de 999 ou le nom d'une autre personne. Utiliser des valeurs variées dans un test ne coûte presque rient et présente certains avantages, abandonnez donc la monotonie. (Remarque : il est peu recommandable d'utiliser le nom de vos ex si votre fiancée du moment travaille avec vous.)

Cela présente un autre avantage. Une erreur plausible est que le programme utilise le champ X alors qu'il aurait dû utiliser le champ Y. Si les deux champs contiennent "Monica", l'erreur ne peut pas être détectée.

L'utilisation de données non réalistes To top of page

Il est courant d'utiliser des données inventées dans les tests. Ces données sont souvent incroyablement simples. Par exemple, des noms de clients comme "Mickey", "Snoopy", et"Donald". Parce que ces données ne correspondent pas à ce qu'un véritable utilisateur peut saisir (par exemple, parce que les données sont plus courtes), des anomalies qu'un véritable client verra peuvent échapper au test. Par exemple, ces noms en un mot ne détecteront pas que le code ne prend pas en charge les noms composés comportant un espace.

Il est prudent de faire un petit effort supplémentaire et d'utiliser des données réalistes.

Ne pas remarquer que le code ne fait rien du tout To top of page

Supposez que vous commencez un enregistrement de base de données à zéro, que vous exécutez un calcul qui doit faire en sorte que zéro soit enregistré dans l'enregistrement, puis que vous vérifiez que l'enregistrement est zéro. Qu'est-ce que votre test a démontré ? Le calcul peut très bien ne s'être effectué du tout. Il est possible que rien n'ait été enregistré et le test n'a pas pu le dire.

Cet exemple semble impossible. Mais cette même erreur peut se présenter de façon plus insidieuse. Par exemple, vous pouvez rédiger un test pour un programme d'installation compliqué. Le test vise à vérifier que tous les fichiers temporaires sont supprimés une fois l'installation terminée avec succès. Mais, en raison de toutes les options d'installation, dans ce test, un fichier temporaire en particulier n'a pas été créé. Il est certain que c'est celui que le programme oubliera de supprimer.

Ne pas remarque que le code ne fait pas la bonne chose To top of page

Parfois, un programme fait la bonne chose pour les mauvaises raisons. Prenons ce code comme exemple banal :

if (a < b && c) 
    return 2 * x;
else
    return x * x;

L'expression logique est erronée, et vous avez rédigé un test qui la fait évaluer de façon erronée et prendre la mauvaise branche. Malheureusement, par pure coïncidence, la variable X a la valeur 2 dans ce test. Le résultat de la mauvaise branche est correct par accident - il est le même que ce que le résultat de la bonne branche aurait donné.

Pour chaque résultat attendu, vous devez vous demander s'il existe une façon plausible d'obtenir ce résultat pour la mauvaise raison. Bien que cela soit souvent impossible à savoir, cela est parfois possible.



RUP (Rational Unified Process)   2003.06.15