Les conceptions de test sont réalisées à l'aide d'informations provenant d'une variété de produits, y compris des
produits de conception comme les réalisations de cas d'utilisation, les modèles de conception ou les interfaces de
classificateur. Les tests sont exécutés après la création des composants. Il est courant de créer les conceptions de
test juste avant l'exécution prévue des tests, bien après la création des produits de conception du logiciel. La figure
1 ci-après montre un exemple. Ici, la conception du test commence vers la fin de l'implémentation. Elle s'inspire des
résultats de la conception des composants. La flèche allant d'Implémentation à Exécution du test indique que les tests
ne peuvent pas être exécutés tant que l'implémentation n'est pas terminée.
Fig1 : la conception du test intervient traditionnellement plus tard dans le cycle de vie
Toutefois, ceci n'est pas obligatoire. Bien que l'exécution du test doive attendre que le composant ait été implémenté,
la conception du test peut être réalisée plus tôt. Elle peut être réalisée juste après que le produit de conception est
terminé. Elle peut même être réalisée en parallèle avec la conception du composant, comme indiqué ici :
Fig2 : La conception pilotée par le test aligne chronologiquement la conception du test avec la conception du logiciel
Déplacer l'effort de test "en amont" de cette façon est communément appelé "Conception pilotée par le test" (test-first
design). Quels en sont les avantages ?
-
Quelle que soit l'attention que vous porterez à la conception du logiciel, vous ferez des erreurs. Vous pouvez
laisser passer un fait pertinent. Ou vous pouvez avoir l'habitude de réfléchir d'une façon particulière qui vous
empêche d'envisager certaines alternatives. Ou vous pouvez être tout simplement fatigué et omettre quelque chose.
Faire réviser vos produits de conception par d'autres personnes est utile. Elles peuvent avoir pensé aux faits que
vous avez laissés passer ou bien remarquer un oubli. Le mieux est que ces personnes aient un point de vue différent
du vôtre ; en envisageant la conception différemment, elles verront ce que vous avez omis.
L'expérience a montré que l'optique du test est efficace. Elle est implacablement concrète. Pendant la conception
du logiciel, il est facile de penser à une zone particulière comme "affichant le titre du client actuel" et de
poursuivre sans vraiment y penser. Pendant la conception du test, vous devez décider spécifiquement de ce
que cette zone affichera lorsqu'un client retraité de la Marine qui a ensuite obtenu une maîtrise de droit, insiste
pour qu'on l'appelle "Lieutenant Morton H. Throckbottle (Ret.), Esq". Son titre est-il "Lieutenant" ou "Esquire"
?
Si la conception du test est retardée jusqu'à juste avant de l'exécution du test, comme dans la Figure 1, vous
perdrez probablement de l'argent. Une erreur dans la conception de votre logiciel ne sera pas décelée jusqu'à la
conception du test, quand un testeur dira "Vous savez, je connaissais ce type de la Marine...", créera le test
"Morton" et découvrira le problème. C'est alors qu'une implémentation partiellement ou entièrement terminée devra
être réécrite et qu'un produit de conception devra être mis à jour. Cela aurait coûté moins cher de détecter le
problème avant le début de l'implémentation.
-
Certaines erreurs peuvent être décelées avant la conception du test. A la place, elles seront alors décelées par
l'implémenteur. C'est encore mauvais. L'implémentation doit s'arrêter progressivement alors que l'attention passe
de comment implémenter la conception à ce que devrait être la conception. Cela est gênant, même lorsque les rôles
d'implémenteur et de concepteur sont assurés par la même personne ; et c'est encore plus gênant lorsque ce sont des
personnes différentes. La Conception pilotée par le test aide à améliorer l'efficacité en évitant ce
désagrément.
-
Les conceptions de test aident les implémenteurs d'une autre façon : en clarifiant la conception. Si l'implémenteur
a une question sur la signification de la conception, la conception de test peut servir d'exemple spécifique du
comportement souhaité. Cela aura pour résultat un nombre inférieur de bogues provenant d'une incompréhension de
l'implémenteur.
-
Il y a moins de bogues même si l'implémenteur n'avait pas cette question en tête alors qu'il aurait dû. Par
exemple, il peut y avoir eu une ambiguïté que le concepteur a interprétée inconsciemment d'une façon et
l'implémenteur d'une autre. Si l'implémenteur travaille à la fois à partir de la conception et des instructions
spécifiques sur ce que le composant est supposé faire (à partir des cas de test), le composant aura davantage de
chances de faire réellement ce qu'on attend de lui.
Voici quelques exemples pour vous donner un aperçu de la Conception pilotée par le test.
Supposez que vous créez un système pour remplacer l'ancienne méthode consistant à "demander à la secrétaire" pour
l'affectation des salles de réunion. L'une des méthodes de la classe MeetingDatabase s'appelle
getMeeting, et a la signature suivante :
Meeting getMeeting(Person, Time);
Une fois le nom de la personne et l'heure saisis, getMeeting renvoie que telle personne est prévue pour assister
à la réunion à telle heure. Si la personne n'a rien de prévu, elle renvoie l'objet Meeting : libre. Il
existe des cas de test très simples :
-
La personne n'a aucune réunion à un moment donné. L'objet libre est-il renvoyé ?
-
La personne a une réunion à cette heure. La méthode renvoie-t-elle la bonne réunion ?
Ces cas de test n'ont rien de passionnant, mais ils doivent en fin de compte être essayés. Ils peuvent également être
créés maintenant, en rédigeant le code de test réel qui sera exécuté ultérieurement. Le code Java pour le premier test
peut ressembler à cela :
// if not in a meeting at given time, // expect to be unscheduled. public void testWhenAvailable() { Person fred = new Person("fred"); Time now = Time.now(); MeetingDatabase db = new MeetingDatabase(); expect(db.getMeeting(fred, now) == Meeting.unscheduled); }
Mais il y a des idées de test plus intéressantes. Par exemple, cette méthode recherche une correspondance. Chaque fois
qu'une méthode recherche quelque chose, c'est une bonne idée de se demander ce qui devrait se passer si la recherche
trouve plusieurs résultats. Dans ce cas, cela revient à se demander : "Une personne peut-elle être présente à deux
réunions en même temps ?". Cela semble impossible, mais si l'on pose la question à la secrétaire, on peut obtenir une
réponse surprenante. Il apparaît que certains cadres ont assez souvent deux réunions prévues au même moment. Leur rôle
est de passer à la réunion, de "rallier les troupes" pendant un certain temps, puis de passer à la deuxième. Un système
qui n'avait pas prévu ce comportement sera inutilisé, du moins partiellement.
Voici un exemple de conception pilotée par le test réalisée au niveau de l'implémentation et qui met le doigt sur un
problème d'analyse. Il y a plusieurs remarques à faire :
-
Vous pourriez espérer qu'une définition et une analyse efficaces des cas d'utilisation auraient permis d'identifier
cette exigence. Dans ce cas, le problème aurait été évité "en amont" et getMeeting aurait été conçu
différemment. (Il n'aurait pas renvoyé une réunion mais une série de réunions.) Mais l'analyse manque toujours
certains problèmes, et il vaut mieux qu'ils soient découverts pendant l'implémentation qu'après le
déploiement.
-
Dans de nombreux cas, les concepteurs et implémenteurs n'auront pas la connaissance du domaine qui leur permettra
de déceler de tels problèmes ; ils n'auront pas l'occasion ou le temps d'interroger la secrétaire. Dans ce cas, la
personne qui conçoit les tests pour getMeeting se demandera s'il existe des cas dans lesquels deux réunions
doivent être renvoyées, réfléchira un moment, et conclura que non. Ainsi, la Conception pilotée par le test ne
décèle pas tous les problèmes mais le simple fait de se poser les bonnes questions augmente la chance de trouver un
problème.
-
Certaines des mêmes techniques de test qui s'appliquent pendant l'implémentation s'appliquent aussi à l'analyse. La
Conception pilotée par le test peut être faite par des analystes mais ce n'est pas l'objet de cette page.
Le deuxième des trois exemples est un modèle de diagramme d'état-transition d'un système de chauffage.
Fig3 : diagramme d'état-transition chauffage, ventilation et climatisation
Un ensemble de tests traversera tous les arcs de ce diagramme d'état-transition. Un test peut commencer avec un système
au ralenti, intégrer un événement Trop chaud, faire échouer le système pendant l'état Refroidissement/Marche, résoudre
la panne, intégrer un autre événement Trop chaud puis refaire fonctionner le système au ralenti. Etant donné que tous
les arcs n'entrent pas en jeu, d'autres tests sont nécessaires. Ces types de tests recherchent différents types de
problèmes d'implémentation. Par exemple, en traversant chaque arc, ils vérifient si l'implémentation en a laissé un de
côté. En utilisant des séquences d'événements qui ont des chemins de panne suivis de chemins qui doivent aboutir
positivement, ils vérifient que le code de traitement des erreurs arrive bien à résoudre des résultats partiels qui
pourraient affecter les calculs ultérieurs. (Pour en savoir plus sur le test des diagrammes d'état-transition, voir Instructions relatives au produit : Idées de test pour diagramme d'état-transition et
d'activités.)
Le dernier exemple utilise une partie d'un modèle de conception. Il y a une association entre un créditeur et une
facture, où tout créditeur donné peut avoir plus d'une facture impayée.
Fig4 : Association entre les classes créditeur et facture
Les tests basés sur ce modèle mettront le système à l'épreuve quand un créditeur n'aura aucune facture, une facture et
un grand nombre de factures. Un testeur se demandera également s'il existe des situations dans lesquelles une facture
doit être associée à plusieurs créditeurs, ou dans lesquelles une facture n'a aucun créditeur. (Il est possible que les
personnes qui gèrent le système papier que le système informatique doit remplacer utilisent des factures sans créditeur
pour le suivi du travail en cours). Dans ce cas, ce serait un autre problème qui aurait dû être décelé dans l'analyse.
La Conception pilotée par le test peut être réalisée soit par l'auteur de la conception soit par quelqu'un d'autre.
Mais c'est généralement l'auteur qui s'en charge. L'avantage est que cela réduit les coûts de communication. Le
concepteur de produit et le concepteur de test n'ont pas besoin de s'expliquer mutuellement les choses. De plus, un
concepteur de test séparé devra passer du temps pour bien prendre connaissance de la conception alors que le concepteur
d'origine la connaît déjà. Enfin, beaucoup de ces questions (comme "Que se passe-t-il si le compresseur tombe en panne
à l'état X ?")sont des questions naturelles à poser pendant la conception du produit logiciel et la conception du test.
La même personne peut donc les poser une seule fois et écrire les réponses sous la forme de tests.
Il existe cependant des inconvénients. Le premier est que le concepteur de produits est, dans une certaine mesure,
aveugle par rapport à ses propres erreurs. Le processus de conception de test révélera une partie de ce qu'il ne voit
pas mais probablement pas autant que ce que trouverait une autre personne. L'importance de ce problème semble varier
énormément d'une personne à l'autre et est souvent liée à l'expérience du concepteur.
Un autre inconvénient d'utiliser la même personne pour la conception du logiciel et du test est l'absence de
parallélisme. Alors que l'affectation des rôles à des personnes différentes nécessitera un effort total supérieur, cela
aura certainement pour conséquence un gain de temps sur le calendrier du projet. Si les personnes sont impatientes de
terminer la conception et de passer à l'implémentation, passer du temps pour la conception du test peut être frustrant.
Plus important encore, le travail a tendance à être bâclé pour passer à autre chose.
Non. La raison est que toutes les décisions ne sont pas prises à la phase de conception. Les décisions prises pendant
l'implémentation ne seront pas bien testées par les tests créés à partir de la conception. Un exemple classique est un
programme permettant de trier des tableaux. Il existe de nombreux algorithmes de tri différents avec des avantages
différents. Quicksort est généralement plus rapide qu'un tri par insertion sur les grands tableaux mais il est souvent
plus lent sur les petits tableaux. Ainsi, un algorithme de tri peut être implémenté pour utiliser Quicksort pour les
tableaux possédant plus de 15 éléments et le tri par insertion autrement. Cette division du travail peut être invisible
à partir des produits de conception. Vous pouvez la représenter dans un produit de conception mais le concepteur
peut avoir décidé qu'il n'était pas utile de rendre compte de telles décisions explicites. Etant donné que la taille du
tableau ne joue aucun rôle dans la conception, la conception du test peut utiliser par inadvertance uniquement des
petits tableaux, ce qui signifie qu'aucun code Quicksort ne sera testé.
Comme autre exemple, considérez cette fraction d'un diagramme de séquence. Elle montre un ResponsableSécurité en
train d'appeler la méthode log() de StableStore. Dans ce cas, toutefois, le log() renvoie une
panne qui oblige le ResponsableSécurité à appeler Connection.close().
Fig5 : Instance de diagramme de séquence de ResponsableSécurité
Cela est un bon rappel pour l'implémenteur. Chaque fois que log() échoue, la connexion doit être fermée. La
question pour le test est de savoir si l'implémenteur l'a vraiment fait, et s'il l'a fait correctement, dans
tous les cas ou seulement dans certains cas. Pour répondre à la question, le concepteur de test doit
trouver tous les appels vers StableStore.log() et s'assurer que chacun de ces points d'appel reçoit une panne à
gérer.
Cela peut sembler étrange d'exécuter un tel test, étant donné que vous venez d'examiner le code qui appelle
StableStore.log(). Ne pourriez-vous pas seulement vérifier s'il gère les pannes correctement ?
Peut-être qu'une inspection suffirait. Mais le code de gestion des erreurs est réputé pour être sujet aux erreurs car
il repose souvent implicitement sur l'hypothèse que l'erreur a eu des répercussions. L'exemple classique est un code
qui gère les anomalies d'affectation. Voici un exemple :
while (true) { // top level event loop try { XEvent xe = getEvent(); ... // main body of program } catch (OutOfMemoryError e) { emergencyRestart(); } }
Ce code tente de récupérer après une erreur de type 'mémoire saturée' en effectuant un nettoyage (pour libérer de la
mémoire), puis en continuant de traiter les événements. Supposons que cette conception est acceptable.
emergencyRestart prend soin de ne pas affecter de mémoire. Le problème est que emergencyRestart appelle
une routine d'utilitaire, qui en appelle une autre, qui en appelle une autre, ce qui affecte un nouvel objet. Le
problème est qu'il n'y a pas de mémoire, et donc le programme échoue complètement. Ces types de problèmes sont
difficiles à détecter à travers une inspection.
Jusqu'à ce point, nous avons implicitement supposé que alliez concevoir la plus grosse partie des tests aussi tôt que
possible. C'est-à-dire que vous alliez dériver tous les tests que vous pouvez du produit de conception, n'ajoutant
ensuite que les tests basés sur les éléments internes à l'implémentation. Cela peut ne pas être approprié dans la phase
d'élaboration, car des tests aussi complets peuvent ne pas être alignés avec les objectifs de l'itération.
Supposons qu'un prototype architectural est créé pour démontrer la faisabilité du produit à des investisseurs. Il peut
se baser sur quelques instances de cas d'utilisation clés. Le code doit être testé pour voir s'il les prend en charge.
Mais la création d'autres tests peut-elle faire des dégâts ? Par exemple, il peut être évident que le prototype ignore
des cas d'erreurs importants. Pourquoi ne pas documenter le besoin de traiter ces erreurs en rédigeant des cas de test
qui les mettront à l'épreuve ?
Mais que se passe-t-il si le prototype fait son travail et révèle que l'approche architecturale ne fonctionnera pas ?
L'architecture sera ensuite abandonnée, ainsi que tous les tests pour la gestion des erreurs. Dans ce cas, l'effort de
conception de tests n'aura abouti à rien. Il aurait mieux valu attendre et ne concevoir que les tests nécessaires pour
vérifier que le prototype démontre le bien-fondé de la conception.
Cela peut sembler minime mais les effets psychologiques en jeu sont importants. La phase d'élaboration consiste à
aborder les principaux risques. Toute l'équipe du projet doit être concentrée sur ces risques. Demander à des personnes
de se concentrer sur des questions de moindre importance détourne l'équipe de son objectif.
Alors, quand la conception pilotée par le test peut-elle être utilisée avec succès dans la phase d'élaboration ? Elle
peut jouer un rôle important dans l'exploration adéquate des risques architecturaux. En déterminant comment l'équipe
saura si un risque s'est concrétisé ou a été évité, vous clarifiez le processus de conception et vous augmentez vos
chances de créer une meilleure architecture du premier coup.
Pendant la phase de construction, les produits de conception prennent leur forme finale. Toutes les réalisations de cas
d'utilisation requises sont implémentées, tout comme les interfaces pour toutes les classes. Etant donné que l'objectif
de la phase est l'achèvement, une conception pilotée par le test complète est appropriée. Les événements ultérieurs
invalideront peut-être quelques tests, mais pas nécessairement.
Les phases de création et de transition sont généralement moins axées sur les activités de conception qu'il convient de
tester. Quand c'est le cas, la conception pilotée par le test s'applique. Par exemple, elle peut être utilisée dans le
travail visant à démontrer le bien-fondé de la conception dans la phase de création. Comme avec les tests des phases de
construction et d'élaboration, elle doit être alignée avec les objectifs de l'itération.
|