Article publié dans Linux Magazine 88, novembre 2006.
Copyright © 2006 - Sébastien Aperghis-Tramoni, Philippe Blayo
Despite its hacker reputation, the Perl community has a very strong commitment to unit and regression testing.
-- Andrew Hunt et David Thomas, The Pragmatic Programmer [8]
Cet article débute une série consacrée aux différentes techniques et méthodologies de test en Perl. On verra que la communauté Perl s'est dotée d'outils qui font sa singularité dans le vaste domaine des techniques de tests.
How do you test your software? Write an automated test.
-- Kent Bech, 2002
Un programmeur se trouve généralement dans l'une des deux situations suivantes :
Écrire un nouveau code ;
Reprendre un ancien code et le modifier. On parle de code historique ou de code hérité (legacy code).
Dans ces deux cas, pourtant très différents, la présence d'une suite de tests peut s'avérer décisive.
Mais d'abord qu'appelle-t-on un test ?
ok 1+1 == 2;
Ce test minimal vérifie que 1 + 1 vaut 2. Ça a l'air simple. En quoi est-ce si important ?
Aucun développeur professionnel normalement constitué ne livre une modification, aussi petite soit-elle, sans tester. Mais tester ses changements n'est pas la même chose qu'avoir des tests. Un Test est une procédure qui mène à une acceptation ou un rejet. tester, le verbe, évoque des actions manuelles, comme cliquer sur des boutons, dont on évalue les résultats à l'écran. Avoir des tests, c'est pérenniser ces actions sous une forme automatisée, qu'on peut rejouer n'importe quand.
Pourquoi cette automatisation fait-elle la différence ?
Old tests don't die. They become regression tests
-- Michael Schwern et chromatic[2]
Chaque fois qu'on modifie du code, on risque aussi de le casser. C'est ce qu'on appelle une régression. C'est pourquoi les tests sont parfois qualifiés de tests de non-régression. Avoir des tests c'est disposer d'un filet, qui empêche de tomber trop bas.
Plus tôt un problème est détecté, plus sa cause est facile à déterminer. L'automatisation des tests permet de les rejouer plus souvent. Si un test ne passe plus, l'erreur réside dans les modifications depuis son dernier succès. Si une seule ligne a changé, l'erreur s'y trouve forcément.
Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.
-- Brian Kernighan, cité par Damian Conway[9]
En général, quand un défaut apparaît, ce qui prend le plus de temps n'est pas de le corriger mais de déterminer sa provenance. Capitaliser ce temps passé sous forme d'un test qui le reproduit évite d'avoir à le pister à nouveau.
C'est le cas où on répugne le moins à automatiser un test, car on mesure tout le temps qu'il a coûté. L'impact psychologique est d'autant plus fort si c'est la deuxième fois qu'il se manifeste.
Une bonne pratique consiste à d'abord ajouter des tests dans le but de reproduire l'erreur pour ensuite modifier le code fautif afin qu'il passe les tests. L'intérêt de cette pratique, écrire les tests d'abord, ne se limite pas à la reproduction des bugs.
N'est-ce pas le rôle de la documentation ? Justement, certains projets vont jusqu'à utiliser les tests comme seule description du comportement.
Les tests offrent mécaniquement une image plus à jour du comportement d'un code
que sa documentation, et constituent de manière naturelle des exemples de son
utilisation. Quand on vient d'installer un nouveau module depuis CPAN, il est
bien pratique d'en trouver différentes utilisations possibles dans le répertoire
t/
. Ces exemples sous forme de tests offrent une garantie -- ils peuvent être
rejoués à tout moment -- que des exemples de la documentation n'offrent pas.
Cette vision doit cependant être nuancée : sur CPAN, beaucoup de
répertoires t/
restent hantés par la magie noire des origines et
ne jouent aucun rôle de documentation.
Une description sous forme de tests peut aussi aider vis-à-vis de l'extérieur. Prenons l'exemple d'une interface qui ne répond pas à sa spécification. Elle est peut-être utilisée avec ce comportement erroné. On peut aussi ignorer comment elle est utilisée, par exemple si une entité extérieure est concernée. Même -- surtout -- si on réécrit complètement le code, on doit s'assurer que le comportement demeure identique. C'est ce qu'on appelle reproduire les défauts ou être bug compatible.
On se trouve dans un cas où les tests et le code peuvent correspondre et avoir tort tous les deux, mais où la nécessité commande de faire évoluer les spécifications plutôt que le code et ses tests.
Ce n'est pas l'approche préconisée par le projet Phalanx[3], qui place les tests entre le code et sa documentation. Les trois doivent correspondre : la documentation décrit le code qui passe les tests. Le but est de compléter et corriger la documentation, écrire des tests qui couvrent le plus possible de code, et éventuellement corriger les nouveaux bugs ainsi mis en évidence. Le projet Phalanx sera abordé plus en détail dans un futur article.
Disposer d'une liste de tests à faire passer aide à déterminer ce qui marche déjà et ce qui reste à faire. La non-régression est un aspect important, car une fonctionnalité testée il y a six mois peut avoir été cassée par des ajouts réalisés depuis.
Diminuer les risques de régressions rend ainsi plus réalistes les estimations et augmente la visibilité.
À l'inverse, la situation la pire est celle qui consiste à repousser les tests à la fin d'un projet (on parle de coût caché). Une remarque du type « C'est fini à 99%, il faut juste tester » est symptomatique d'une telle pathologie.
On croise beaucoup d'appellations différentes, qui se recouvrent plus ou moins. On en citera deux :
On peut séparer les tests selon leur rôle / provenance : tests développeurs pour la maîtrise d'œuvre, tests de recette pour le client et la maîtrise d'ouvrage.
Les tests développeur permettent de bien construire le logiciel. Les tests de recette assurent qu'il correspond au désir du client.
On peut aussi les classer selon leur granularité : tests applicatifs pour des vérifications au niveau d'un logiciel tout entier, tests unitaires pour les éléments d'une granularité plus fine qui assurent son fonctionnement interne (méthodes, modules).
Les tests applicatifs sont parfois appelés tests fonctionnels. Ils ont valeur de tests de recette s'ils sont validés par le client.
Enfin, des tests écrits en connaissant le code sont dits boîte blanche (white box, par opposition à black box) ou boîte transparente (glass box, par opposition à l'opacité d'une boîte noire).
Perl dispose depuis longtemps (Perl 1 l'utilisait déjà) d'une
technique de test assez simple mais assez efficace. Elle consiste
à exécuter des scripts qui vont réaliser des suites de tests basiques
pour vérifier le bon fonctionnement du programme testé. Chaque test
signale s'il s'est bien exécuté ou pas en affichant sur la sortie
standard "ok"
ou "not ok"
. Cela fonctionne donc sur un principe
similaire aux checklists effectuées par les ingénieurs de l'ESA
ou de la NASA (où Larry Wall a d'ailleurs travaillé).
Illustrons avec un exemple trivial :
$a = 1 + 1; print $a == 2 ? "ok\n" : "not ok\n";
Et pour connaître l'avancement des tests en cours, il suffit que le script de test affiche avant cela le nombre total de tests qu'il compte exécuter :
print "1..2\n"; # pour exécuter deux tests
Cela permet de savoir où on en est et de vérifier si, à la fin du script, on a bien exécuté le nombre de tests annoncés.
L'intérêt de cette sortie quelque peu monotone est qu'elle est extrêmement simple à lire par un autre programme qui va présenter les choses de manière moins verbeuse, par exemple avec une barre de progression. Cette sortie devient donc un véritable protocole (même s'il est à sens unique) car les programmes de tests et les programmes d'analyse doivent se mettre d'accord sur le format, la manière de présenter le résultat de chaque test. La version actuelle du protocole offre d'autres fonctionnalités que nous allons maintenant présenter plus en détails.
Le protocole présenté au paragraphe précédent correspond à la
toute première version du protocole, toujours valide, mais qui
depuis a été étendu pour fournir de nouvelles sémantiques.
Andy Lester lui a récemment donné le petit nom de TAP[4], pour
Test Anything Protocol, suite à une séance de brainstorming
sur AIM assez mémorable[5]. TAP est actuellement défini par le
module Test::Harness
(maintenu par Andy Lester), qui intercepte
et interprète tout ce qu'un script affiche sur sa sortie standard,
laissant passer tout message imprimé sur la sortie d'erreur.
Perl Testing: A Developer's Notebook
Faisant partie de la série des Developer's Notebooks, présentés sous la forme d'un cahier (avec même la marque de la tasse de café), PTDN constitue un vrai catalogue, présentant succintement une myriade de modules de test et abordant des sujets très variés, tels que le test de modules liés aux bases de données ou au serveur web Apache, mais entrant rarement dans les détails avancés. S'il lui manque ainsi une présentation des méthodologies de réalisation des tests, il fait néanmoins office de bonne introduction aux tests automatisés en Perl.
Le plan est la première ligne attendue. Il doit indiquer le
nombre de tests à effectuer et est de la forme "1..N"
.
Si N vaut zéro, l'exécution est arrêtée. Cela permet d'éviter
d'exécuter des tests si on sait qu'ils vont échouer (parce
que le système n'est pas supporté, qu'une fonctionnalité est
absente ou que l'utilisateur qui exécute les tests ne dispose
pas de privilèges suffisants pour une partie des tests). Toute
autre valeur positive indique le nombre de tests attendus. Dans
les cas où le nombre de tests n'est pas connu à l'avance (parce
qu'il dépend de données variables par exemple), le plan peut
alors apparaître en dernière ligne de la sortie, afin de vérifier
a posteriori si l'ensemble des tests s'est bien déroulé.
Cela nécessite par contre une version pas trop ancienne de
Test::Harness
, qui considère sinon l'absence du plan comme
une erreur.
Les lignes de test, ou points de tests, constituent la majeure partie d'une sortie TAP. Chaque ligne a le format suivant :
ok 12 - description du test # directive supplémentaire
et se décompose de la manière suivante :
un état, ok
ou not ok
qui indique si le test a réussi ou non.
un numéro, qui doit suivre l'état. Comme il est optionnel, ce sera sinon à l'interpréteur de maintenir un compteur interne pour vérifier que le compte est bon.
une description, optionnelle, qui indique brièvement ce que
fait le test. Elle ne doit évidemment pas commencer par un nombre
(qui pourrait sinon être pris comme numéro du test), d'où une
tradition de la faire commencer par "- "
pour éviter les
problèmes.
une directive, optionnelle, qui commence par le caractère dièse #
.
Actuellement, seules deux directives sont reconnues, TODO
et SKIP
.
Les directives permettent de modifier l'interprétation de l'état d'un test. Les deux directives supportées sont :
TODO
, qui permet de marquer le test comme étant "à faire".
Une explication peut suivre afin d'indiquer la raison.
not ok 11 # TODO voyager plus vite que la lumière
Il peut s'agir d'une fonctionnalité à ajouter ou d'un bug à corriger. On n'attend pas d'un tel test qu'il réussisse, puisqu'il est censé échouer. Si le test réussit, il est compté comme bonus, mais le développeur doit alors marquer ce test comme normal.
SKIP
, qui permet de sauter l'exécution de ce test.
Une explication peut suivre afin d'indiquer la raison.
ok 21 # SKIP nécessite plus d'énergie
Il s'agit typiquement de sauter les tests qui ne peuvent pas être
exécutés sur la plate-forme courante par manque de fonctionnalité,
et qui produiraient donc de faux négatifs, par exemple : modules
non installés, fonctionnalités indisponibles sur le système
d'exploitation (comme fork()
, les liens symboliques, un affichage
graphique), connexion à l'Internet ou à une base de données.
Outre les lignes de tests, TAP connaît encore deux autres types
de lignes. Le premier est l'arrêt d'urgence, qui se déclenche en
affichant "Bail out!"
sur la sortie. L'exécution s'arrête net,
laissant tomber le reste des tests. Une explication peut suivre
afin d'indiquer la raison de l'abandon.
Bail out! Réserves de cosmogol épuisées
Les diagnostics sont le dernier type de lignes reconnues par TAP.
Il s'agit simplement de texte libre permettant d'afficher des informations
diverses telles que les modules utilisés, les programmes exécutés, etc.
Une ligne de diagnostic doit commencer par le caractère dièse #
.
... ok 12 - équipage à bord ok 13 - capitaine à la barre ok 14 - signal du départ # et les Shadoks pompaient, pompaient... # et le vaisseau commençait à bouger ok 15 - première phase du décollage # le suspense est terrible # ... # la suite au prochain épisode !
Toute autre ligne n'est pas valide. Actuellement, le comportement
de Test::Harness
est d'ignorer silencieusement de telles lignes
par souci de compatibilité avec l'existant mais il n'est pas garanti
que ce comportement soit préservé dans le futur.
Conformance
On peut remarquer que, bien qu'il ait été conçu de manière a priori indépendante, TAP offre de fait des sémantiques très proches de celles définies par l'environnement de test DejaGnu qui est lui-même une extension du standard POSIX 1003.3[10].
Évidemment, vous n'aurez pas à écrire vous-même des sorties au format TAP (il s'agit de respecter la paresse de chacun), mais vous serez certainement amenés à lire les résultats de vos tests, donc autant comprendre ce qui s'affiche à l'écran. La sortie sera en pratique générée par des modules appropriés, que nous allons décrire durant le reste de cet article.
Toutefois, si vous héritez d'un code vraiment vieux (ou si vous
regardez les tests de certains modules assez anciens), vous
constaterez que certains n'utilisent aucun module de test mais
écrivent directement les "ok"
et "not ok"
à coups de print
.
On trouve généralement des commentaires parlant de magie noire
pour tester le chargement d'un module... Ça en dit long !
Fort heureusement, au fur et à mesure des années et de l'évolution
de ce secteur qu'est le test des logiciels, des méthodologies ont
été mises au point et des modules ont été développés afin de
simplifier l'écriture et la maintenance des programmes de tests.
Les modules de génération de TAP les plus utilisé sont sans conteste
Test
et Test::More
. On peut aussi signaler Test::Simple
,
mais tant son utilisation que son intérêt sont marginaux, et
comme le reste de l'article va être consacré à la présentation de
Test::More
, parlons rapidement de Test
.
Test
est un vieux module, intégré à la distribution standard
de Perl depuis la version 5.005. Il offre la majeure partie des
fonctionnalités du protocole TAP, mais son API n'est pas des
plus intuitive, offrant principalement une fonction ok()
dont
la sémantique exacte varie en fonction du nombre de paramètres.
C'est utilisable, mais ouvre la voie à nombre d'erreurs, certaines
difficiles à repérer quand on n'a pas l'habitude. C'est pour cette
raison que son utilisation est découragée au profit de Test::More
,
dont l'interface est bien plus naturelle.
Ouvrons une parenthèse afin d'insister sur un point qui pourrait
paraître un détail, mais qui a en réalité des conséquences assez
importantes : la séparation entre le programme de test proprement
dit, qui génère la suite de ok
/not ok
, et le programme
d'analyse de la sortie du premier, l'interpréteur TAP.
La séparation entre ces deux composants est à l'origine du protocole TAP. Sinon, il suffirait de stocker les informations dans un objet et d'afficher le résultat à la fin de l'exécution du test, comme c'est le cas des programmes basés sur JUnit en Java. Certes, mais qu'arrive-t-il si le programme de test plante et meurt ? Rien, aucune sortie n'est générée puisque le code en charge de l'affichage des résultats a été purgé avec le reste du processus.
C'est là que la séparation entre le générateur et l'interpréteur TAP devient très intéressante : comme c'est l'interpréteur qui lance l'exécution du générateur, il n'est pas affecté si ce dernier meurt et peut ainsi détecter que le générateur a inopinément quitté. Il peut même indiquer après quel test il s'est planté, simplifiant ainsi le travail de recherche du code fautif.
Autre point positif, l'interpréteur peut préparer l'environnement
du programme de test afin de positionner certaines variables
(comme PATH
ou LD_LIBRARY_PATH
) ou options de perl(1)
pour qu'il puisse trouver des bibliothèques ou modules non
installés, voire, pourquoi pas, afin de l'isoler dans un
compartiment logiciel étanche.
Une autre conséquence de la séparation entre générateur et
interpréteur est que les programmes respectifs n'ont pas besoin
d'être dans le même langage. Rien n'empêche ainsi un programme
en Java d'écrire du TAP sur sa sortie standard, qui peut être
interprété par Test::Harness
en Perl. D'ailleurs, c'est même
tellement facile que de plus en plus de programmeurs proposent
des portages de Test::Simple
ou Test::More
sur leur langage
favori : C, shell, Java, PHP, Lisp, Haskell, JavaScript.
Et avant cet engouement qui est tout de même assez récent,
les développeurs de la CLR, la machine virtuelle de Microsoft
destinée à exécuter les logiciels dit « .Net », utilisaient
déjà Test::Harness
pour piloter leurs tests de non-régression.
Voici donc un moyen subversif et détourné d'introduire Perl dans
un environnement de développement qui n'utilise pas ce langage :
car s'il est très facile de porter les sémantiques de Test::More
sur n'importe quel langage digne de ce nom, il n'existait jusqu'à
présent qu'un seul interpréteur TAP, Test::Harness
. Mais comme
celui-ci a des limitations assez sévères (il dérive après tout d'un
programme datant de presque 20 ans !), et que la tentative de
refonte a été jugée infructueuse, un hackathon a été organisé lors
de YAPC::NA 2006 à Chicago pour discuter de l'avenir de TAP.
Les participants ont décidé de rédiger les spécifications d'un
nouvel analyseur TAP, en tenant compte des besoins actuels et futurs,
tout en préservant la compatibilité arrière. L'activité est telle
qu'en moins d'un mois, pas moins de quatre analyseurs différents
sont en développement, dont un en Perl6 et un en Parrot. Le but est
de faciliter l'utilisation de Perl comme moteur de test avec les
autres langages d'une part et d'offrir le maximum de flexibilité
dans la présentation des résultats d'autre part. Un article ultérieur
montrera ainsi comment écrire son propre Test::Harness
et
personnaliser ses rapports.
Comme annoncé précédemment, Test::More
offre une interface assez
naturelle à utiliser, basée sur un ensemble de fonctions répondant
à des sémantiques bien précises. Voyons lesquelles.
plan()
La fonction plan()
permet de spécifier le plan du script de test.
Elle doit bien sûr n'être invoquée qu'une et une seule fois au cours
du flot d'exécution sous peine d'erreur fatale.
plan()
supporte trois options mutuellement exclusives, qui peuvent
aussi être passées directement en paramètre lors de l'importation de
Test::More
.
tests => $NbTests
correspond à ce qu'on connaît déjà, et permet
d'indiquer le nombre de tests prévus. On peut indiquer une expression,
du moment qu'elle est calculable au moment de l'exécution de la ligne
correspondante.
Si le nombre de tests est fixe et connu :
use Test::More; plan tests => 15;
ou
use Test::More tests => 15;
Si le nombre de tests est calculable :
use Test::More; my @data = ( ... ); plan tests => @data * 5 + 12;
no_plan
est à utiliser si le nombre de tests n'est pas connu ou
pas calculable à l'avance. Test::More
se chargera alors de générer
un plan a posteriori à la fin de l'exécution du script.
use Test::More; plan 'no_plan';
ou
use Test::More 'no_plan';
skip_all => $Raison
permet de sauter l'exécution d'un script
qui ne peut pas réussir pour une raison déterminée (système non
supporté, droits insuffisants).
use Test::More; plan skip_all => "Ce n'est même pas la peine..." if $^O eq "MSWin32";
ok()
La fonction ok()
permet de tester une valeur booléenne, et accepte
en plus un argument optionnel, la description du test.
ok( defined $reservoir, "\$reservoir est défini" ); ok( $reservoir->type_carburant() eq 'cosmogol', "type de carburant" );
is()
, isnt()
Les fonctions is()
et isnt()
vérifient, comme leur nom l'indique,
que deux valeurs sont respectivement égales ou différentes. Ces fonctions
attendent trois arguments : les deux premiers sont respectivement la
valeur à tester et la valeur attendue, le troisième est la description
du test. La comparaison entre les deux valeurs est effectuée avec eq
(pour is()
)ou ne
(pour isnt()
).
is( $reservoir->type_carburant(), 'cosmogol', "type de carburant" ); isnt( ref $shadok, 'Gibi', "un Shadok n'est pas un Gibi" );
Vous aurez remarqué que l'exemple de is()
est équivalent au second exemple
de ok()
donné précédemment. Exact. Quel est l'intérêt alors d'utiliser
la fonction is()
? Simple, comme elle comporte la sémantique que les
deux valeurs qu'on lui passe doivent être identiques, elle peut en retour
fournir des informations supplémentaires en cas d'échec du test :
not ok 17 - type de carburant # Failed test (t/reservoir.t at line 53) # got: 'essence ordinaire' # expected: 'cosmogol'
Ce qui est quand même plus intéressant car on sait immédiatement où chercher l'erreur (voire pourquoi cette erreur se produit).
Attention toutefois à ne pas tenter de tester les valeurs booléennes avec ces fonctions. En effet, même si
is( exists $hash{key}, 1, "clé 'key' présente dans %hash" );
semble une bonne idée (et qui, dans cet exemple précis, va fonctionner),
la sémantique du test n'est plus la même. Ici, on ne vérifie pas si
exists $hash{key}
est vrai (c'est-à-dire savoir si la clé key
est
présente dans %hash
), mais seulement si cette expression renvoie 1.
C'est très différent car si, sur le plan technique, cela peut fonctionner
(ce qui n'est pas garanti), sur le plan sémantique le test est incorrect.
Pourquoi n'est-ce pas garanti ? Dans quels cas exists
pourrait
renvoyer une valeur différente de 1 ? Par exemple si le hash est lié
(tie()
), la fonction EXISTS()
sous-jacente peut renvoyer n'importe
quelle valeur vraie.
Le même piège existe aussi pour les valeurs fausses, qui peuvent être
zéro, la chaîne vide ''
ou undef
. La règle générale à suivre est
donc d'utiliser ok()
pour tester les valeurs booléennes, et is()
(ou isnt()
) pour tester les égalités (ou inégalités).
cmp_ok()
Pour des comparaisons autres que l'égalité, cmp_ok()
est là pour
ça. Cette fonction accepte quatre arguments : la valeur à tester,
l'opérateur de comparaison, la valeur attendue et la description du
test (optionnelle).
cmp_ok( $passagers, '>', 0, "au moins un passager" ); cmp_ok( $passagers, '<=', $maximum, "mais dans la limite des sièges disponibles " );
L'avantage d'utiliser cmp_ok()
plutôt que de coder la comparaison
dans un ok()
est quand cas d'échec, l'affichage offre plus
d'informations :
ok 21 - au moins un passager not ok 22 - mais dans la limite des sièges disponibles # Failed test (t at line 7) # '350' # <= # '300'
like()
, unlike()
Les fonctions like()
et unlike()
permettent de vérifier si
une valeur correspond (ou ne correspond pas) à un motif d'expression
régulière. De manière étonnamment peu surprenante, elles attendent
trois arguments : en premier la valeur à tester, puis le motif
de recherche et enfin la description du test. Le motif peut aussi
bien être fourni en utilisant l'opérateur qr/../
qu'en tant que
chaîne de caractères "/../"
afin de rester compatible avec les
versions précédentes de Perl qui n'offraient pas cet opérateur.
like( $numero, '/^(?:bu|zo|meu)(?:ga|bu|zo|meu)$/', "le numéro est-il une séquence valide ?" ); unlike( $message, '/erreur/', "il ne doit pas y avoir d'erreur" );
is_deeply()
La fonction is_deeply()
s'utilise exactement comme is_ok()
,
si ce n'est qu'elle accepte en argument des structures dont elle
peut comparer le contenu en profondeur.
is_deeply( $trajet, [ "Prima", "Dua", "Tierza" ] "vérification du trajet", );
Quoique très pratique pour des structures simples à moyennement
complexes, elle montre rapidement ses limitations sur des
structures véritablement complexes, comportant des objets
trop exotiques pour elle (objets, références de code, fermetures,
descripteurs de fichiers), ou avec un contenu trop variable.
D'autres modules plus adaptés peuvent alors prendre le relais.
Citons Test::Deep
, Test::Differences
pour les modules
de test et Data::Dump::Streamer
comme module générique mais
terriblement puissant.
use_ok()
, require_ok()
Les fonctions use_ok()
et require_ok()
remplacent très
avantageusement ce qui nécessitait avant un peu de magie noire.
Elles permettent comme on peut s'en douter de tester le chargement
et l'importation un module. Leur utilisation est quasiment
identique aux use
et require
habituel.
# chargement d'un fichier require_ok('init.pl'); # chargement d'un module require_ok('Rocket::Engine'); # importation d'un module use_ok('Rocket::Engine');
Une erreur de chargement provoquera par exemple cet affichage :
not ok 3 - require Rocket::Engine; # Failed test (-e at line 1) # Tried to require 'Rocket::Engine'. # Error: Can't locate Rocket/Engine.pm in @INC
Il est toujours possible de passer à use_ok()
des options
d'importation :
# numéro de version minimum use_ok('Rocket::Engine', '3.0'); # options d'Exporter use_ok('Rocket::Engine', ':all'); # options libres use_ok('Rocket::Engine::Init', init => 'auto', preload => 1);
À noter toutefois que contrairement à use
qui s'exécute (et donc
le module qu'il charge) dès la phase de compilation, use_ok()
ne
peut être invoqué que pendant la phase d'exécution. S'il est nécessaire
que le module soit chargé au plus tôt, il faut alors englober l'appel
de use_ok()
dans un bloc BEGIN
:
BEGIN { use_ok('Power') }
Cela devient alors parfaitement équivalent à use Power
.
isa_ok()
La fonction isa_ok()
est le pendant de la méthode isa()
et
permet de vérifier si un objet ou une référence est du type escompté.
Ainsi, pour une référence :
my $hashref = {}; isa_ok( $hashref, 'HASH', '$hashref' );
affiche
ok 51 - $hashref isa HASH
et pour les objets, cela gère l'héritage :
use Test::More tests => 3; package Animal; sub new { return bless {}, shift } package Chien; @ISA = 'Animal'; package main; $chien = Chien->new(); isa_ok( $chien, 'Animal', '$chien' ); isa_ok( $chien, 'Chien', '$chien' ); isa_ok( $chien, 'Cheval', '$chien' );
affiche
ok 1 - $chien isa Animal ok 2 - $chien isa Chien not ok 3 - $chien isa Cheval # Failed test (chien.t at line 13) # $chien isn't a 'Cheval' it's a 'Chien'
Ouf !
can_ok()
La fonction can_ok()
permet de vérifier si un objet ou une classe
dispose des méthodes indiquées :
can_ok( $chien, qw(aboyer manger uriner dormir conduire) );
ce qui compte pour un seul test :
not ok 4 - Chien->can(...) # Failed test (chien.t at line 18) # Chien->can('conduire') failed
Si vous préférez générer un test pour chaque méthode, il suffit de l'encadrer dans une boucle :
can_ok( $chien, $_ ) for qw(aboyer manger uriner dormir conduire);
ce qui produira alors :
ok 5 - Chien->can('aboyer') ok 6 - Chien->can('manger') ok 7 - Chien->can('uriner') ok 8 - Chien->can('dormir') not ok 9 - Chien->can('conduire') # Failed test (chien.t at line 19) # Chien->can('conduire') failed
pass()
, fail()
Comme on peut s'en douter, ces fonctions permettent de signifier directement qu'un test a passé ou a échoué. Elles acceptent comme seul argument (optionnel) le descriptif du test. Évidement, elles sont à employer avec parcimonie.
diag()
La fonction diag()
permet d'écrire des diagnostics de manière propre.
Elle s'utilise un peu comme print()
et se charge toute seule de gérer
les problèmes de retour à la ligne et autres détails inintéressants.
C'est la fonction à utiliser pour transmettre des informations diverses
et variées au travers de TAP :
diag("Le commandant de bord vous souhaite la bienvenue\n", "Veuillez vous préparer au décollage");
affiche
# Le commandant de bord vous souhaite la bienvenue # Veuillez vous préparer au décollage
SKIP
Comme cela a été présenté plus haut, il est possible de sauter un script
de test si on peut détecter qu'il va échouer pour une raison déterminée.
Toutefois, il y a des cas où seuls certains tests du script seraient à
sauter, tout en conservant l'exécution des autres. Test::More
offre
une élégante solution pour gérer ces cas, en proposant l'utilisation de
blocs permettant d'isoler les tests à éviter. La forme générale
d'utilisation est :
SKIP: { skip $raison, $nombre if $condition; # le code de test vient ici }
La fonction skip()
prend deux arguments : le premier est la raison
pour laquelle les tests qui suivent pourraient ne pas être exécutés, le
second est le nombre de tests à sauter. Quand skip()
est exécutée, le
reste du code jusqu'à la fin du bloc n'est donc pas exécuté, et Test::More
se charge de générer les ok
indiquant des tests réussis mais sautés.
C'est pour cette raison qu'il est important de préciser le nombre exact
de tests inclus dans le bloc SKIP
afin que le compte soit bon.
Illustrons avec un exemple :
use LWP::Simple; SKIP: { my $page = get('http://perdu.com/'); skip "Pas d'accès au web", 10 unless $page; # code de test }
Ici, on essaye d'abord de télécharger une page depuis un site web en
utilisant LWP::Simple
. Si cela ne marche pas, ce sera probablement
dû au fait que le programme ne peut pas directement accéder au web, et
en conséquence les 10 tests suivants sont sautés.
Autre exemple, si on a besoin d'un module particulier :
SKIP: { eval 'use Test::Warnings'; skip "Test::Warnings non disponible", 4 if $@; # code de test }
Dans ce cas, les tests qui suivent ont besoin du module Test::Warnings
pour tester les messages d'avertissement, mais comme ce n'est pas un point
crucial, on se contente de les sauter si ce module n'est pas présent.
À noter qu'il est parfaitement possible d'imbriquer les blocs SKIP
,
chacun devant simplement posséder le label "SKIP"
ou Test::More
ne pourra pas faire fonctionner sa magie.
Dernier point, il ne faut pas sauter les tests qui échouent parce qu'il
y a un bug dans votre programme ou que le code n'est pas encore écrit car
ce n'est pas du tout la même sémantique. Test::More
offre justement
un autre mécanisme pour cela, les tests TODO
(à faire).
TODO
Ce mécanisme permet d'exécuter un test dont on s'attend à ce qu'il échoue et de préciser en conséquence qu'on ne veut pas le comptabiliser dans les tests qui ont échoués. La syntaxe d'utilisation est la suivante :
TODO: { local $TODO = $raison; # code de test }
Les tests au sein du bloc TODO
seront exécutés mais ne seront pas
comptés comme des échecs même si c'est le cas. Et pour les tests qui
pourraient en cas d'échec planter le programme ou au mieux laisser
l'environnement trop instable pour le reste du script, il est possible
de les sauter comme dans un bloc SKIP
:
TODO: { todo_skip $raison, $nombre if condition; # code de test }
Cette fois, les tests au sein du bloc ne seront pas du tout exécutés et seront affichés comme des échecs, toujours sans être comptés comme tels.
SKIP
et TODO
?La réponse peut paraître évidente mais une recherche avec Gonzui
montre que parmi les scripts de test sur le CPAN qui utilisent
TODO
, une bonne partie ne l'utilisent pas véritablement de la
manière documentée et conseillée.
Une bonne solution est de considérer que ce qui relève du programmeur
(bug non corrigé, fonctionnalité non codée) doit être traité dans des
blocs TODO
. Ces tests apparaissent alors comme une liste de ce
qui reste à faire. Quand le code a été écrit ou corrigé et que les
tests passent, ils peuvent alors être sortis du bloc TODO
pour
devenir des tests normaux.
Les autres cas, qui sont généralement liés à l'environnement ou aux
réponses de l'utilisateur (connexion à l'Internet ou à une base de
données, disponibilité d'un module ou d'une fonctionnalité de Perl)
sont à traiter dans des blocs SKIP
. Si tout le script dépend
d'une fonctionnalité donnée, le plus simple est alors d'utiliser
plan skip_all
pour éviter son exécution.
Test::Simple
et Test::More
sont inclus dans la distribution
standard de Perl depuis la version 5.8.0 et sont compatibles avec
des versions de Perl aussi anciennes que la 5.004, ce qui devrait
rassurer mêmes les plus conservateurs des administrateurs systèmes.
Toutefois, on ne saurait trop vous conseiller de mettre à jour ces
modules, ainsi que Test::Harness
, en utilisant les versions plus
récentes disponibles sur le CPAN.
Tout ça, c'est bien beau, mais ces sorties restent tout de même bien
verbeuses. Et en présence d'une grande quantité de tests, ça peut défiler
à vive allure. Heureusement, afin de permettre de résumer tout ceci,
le module Test::Harness
est là pour analyser et interpréter pour
nous cette quantité d'informations. Test::Harness
est d'ailleurs
actuellement le seul véritable analyseur et interpréteur de TAP
(à l'exception de Test.Harness
en JavaScript étant donné que ce
langage est pour le moment encore très lié aux navigateurs web).
C'est la raison pour laquelle la spécification de TAP est distribuée
avec ce module.
Comme cela a été précisé avant, le rôle de Test::Harness
est même
de piloter l'exécution des tests afin de recueillir le maximum
d'informations. Ainsi, lorsque vous exécutez "make test"
, vous
invoquez en réalité la fonction test_harness()
avec les paramètres
appropriés. Cette fonction se charge de lancer l'exécution des scripts
de tests passés en paramètres, d'analyser leur sortie et de la résumer
pour n'afficher que les erreurs éventuelles.
Prenons comme exemple le module Regexp::Assemble
de David Landgren.
Voici ce qu'on voit à l'écran pendant l'exécution de la suite de
tests :
$ make test t/00_basic........# testing Regexp::Assemble v0.15 t/00_basic........ok t/01_insert.......ok t/02_reduce.......ok t/03_str..........ok t/04_match........ok 9582/14861
Pour chaque script, Test::Harness
capture la sortie et affiche
un compteur indiquant le nombre de tests réussis et restants (quand
il est connu). Dans le premier script, 00_basic.t
, un diag()
permet de savoir quelle version de Regexp::Assemble
est testée.
Si tout se passe bien, la suite se termine :
$ make test t/00_basic........# testing Regexp::Assemble v0.15 t/00_basic........ok t/01_insert.......ok t/02_reduce.......ok t/03_str..........ok t/04_match........ok t/05_hostmatch....ok t/06_general......ok t/07_pod..........ok t/08_track........ok All tests successful. Files=9, Tests=16120, 18 wallclock secs (12.22 cusr + 0.36 csys = 12.58 CPU)
Test::Harness
affiche un résumé indiquant que tout s'est bien
déroulé, et fournit quelques statistiques. Ici, 16 120 tests ont
été effectués en 18 secondes.
Toutefois, avant d'obtenir le message "All tests successful" sur un large ensemble de systèmes, la route de David a été semée de quelques petites embûches :
$ make test t/00_basic........# testing Regexp::Assemble v0.11 ok t/01_insert.......Can't locate Test/Differences.pm in @INC at t/01_insert.t line 22. BEGIN failed--compilation aborted at t/01_insert.t line 22. dubious Test returned status 2 (wstat 512, 0x200) t/02_reduce.......ok t/03_str..........ok t/04_match........ok t/05_hostmatch....ok 3/12 skipped: Test::File::Contents not installed on this system t/06_general......ok t/07_pod..........ok t/08_track........ok Failed Test Stat Wstat Total Fail Failed List of Failed ---------------------------------------------------------------------------- t/01_insert.t 2 512 ?? ?? % ?? 3 subtests skipped. Failed 1/9 test scripts, 88.89% okay. 0/14503 subtests failed, 100.00% okay. make: *** [test_dynamic] Error 2
On peut voir que pour le script 05_hostmatch.t
, David avait bien
pensé à utiliser un bloc SKIP
pour le cas où le module
Test::File::Contents
qu'il utilise ne serait pas disponible.
par contre il avait oublié de faire de même dans 01_insert.t
avec Test::Differences
, d'où une erreur de compilation.
Test::Harness
se rend compte que le script ne s'est pas exécuté
correctement car il s'est brutalement interrompu et a renvoyé un
statut de sortie différent de zéro.
Une fois cette erreur corrigée, David a ajouté d'autres tests, et
ce faisant de nouvelles erreurs :-)
t/00_basic........# testing Regexp::Assemble v0.14 ok t/01_insert.......ok t/02_reduce.......ok t/03_str.......... # Failed test (t/03_str.t at line 104) # got: '(?:.| )' # expected: '.' # Looks like you failed 1 test of 116. dubious Test returned status 1 (wstat 256, 0x100) DIED. FAILED test 18 Failed 1/116 tests, 99.14% okay t/04_match........ok t/05_hostmatch....ok t/06_general......ok t/07_pod..........ok t/08_track........ok Failed Test Stat Wstat Total Fail Failed List of Failed --------------------------------------------------------------------------- t/03_str.t 1 256 116 1 0.86% 18 Failed 1/9 test scripts, 88.89% okay. 1/16125 subtests failed, 99.99% okay. make: *** [test_dynamic] Error 29
Dans ce cas, le test numéro 18 du script 03_str.t
a échoué.
Comme il s'agit d'un cmp_ok()
, il affiche comme on s'y attend
les valeurs attendue et obtenue.
Comme montré dans les exemples précédents, l'exécution de la suite de
tests se lance par la commande make test
(ou Build test
pour les
utilisateurs de Module::Build
). Pour que cela fonctionne, il est
préférable de respecter une convention concernant le placement et le
nommage des scripts. En effet, ceux-ci doivent avoir l'extension .t
et être placés dans le répertoire t/
de la distribution afin d'être
découverts.
À noter que ces scripts peuvent comporter des options sur la
shebang line (comme -T
pour activer le tainting mode)
sans provoquer d'erreur car lors de l'exécution ces options
seront correctement passées à l'interpréteur perl
.
Bien sûr, ces scripts étant des programmes Perl normaux, il est tout à fait possible de les exécuter un à un, « à la main ». Toutefois il faut alors penser à ajouter au chemin de recherche le répertoire contenant le ou les modules à tester :
$ perl -wIlib t/01api.t
Dans le cas de modules compilés, il faut ajouter d'autres chemins,
mais il est alors plus simple d'utiliser le module blib
qui se
charge de ça :
$ perl -wMblib t/01api.t
ou, ce qui est encore plus simple, d'utiliser la commande prove(1),
installée avec les versions récentes de Test::Harness
:
$ prove -b t/01api.t
Notez que pour ces deux dernier cas, vous ne devez pas oublier d'exécuter
un make
avant afin de regénérer les fichiers dans le répertoire blib/
.
Malgré la longueur de cet article, nous n'avons fait qu'effleurer le sommet de l'iceberg que représentent les tests en Perl. Toutefois il était nécessaire afin de commencer sur des bases connues. Les articles suivants présenteront les méthodologies d'écriture des tests et introduiront des modules de tests fournissant de nouvelles sémantiques afin de répondre à des besoins précis, tout en s'intégrant dans l'environnement standard de test de Perl.
[1] Test::Tutorial - http://search.cpan.org/dist/Test-Simple/lib/Test/Tutorial.pod
[2] Test::Tutorial Presentation - Chromatic et Michael G. Schwern, http://mungus.schwern.org/~schwern/talks/Test_Tutorial/Test-Tutorial.pdf http://www.wgz.org/chromatic/talks/TestTutorial/
[3] Phalanx - http://qa.perl.org/phalanx/
[4] TAP, Test Anything Protocol - http://search.cpan.org/dist/Test-Harness/lib/Test/Harness/TAP.pod
[5] Naming TAP, the Test::Harness protocol - http://use.perl.org/~petdance/journal/22057
[6] Perl Testing Reference Card - http://langworth.com/downloads/perl_test_refcard.pdf
[7] Perl Testing: A Developer's Notebook - Ian Langworth et chromatic, O'Reilly & Associates, 2005, ISBN 0-596-10092-2, http://www.oreilly.com/catalog/perltestingadn/
[8] The Pragmatic Programmer: from journeyman to master - Andrew Hunt et Dave Thomas, Addison-Wesley, 2000, ISBN 0-201-61622-X
[9] Perl Best Practices - Damian Conway, O'Reilly & Associates, 2005, ISBN 0-596-00173-8, http://www.oreilly.com/catalog/perlbp/ ; Disponible en français sous le titre De l'art de programmer en Perl (O'Reilly 2006, ISBN 2-84177-369-8).
[10] DejaGnu et POSIX 1003.3 - http://www.gnu.org/software/dejagnu/manual/x47.html
Sébastien Aperghis-Tramoni <sebastien@aperghis.net>, "Maddingue" - {Marseille,Sophia}.pm
Sébastien Aperghis-Tramoni est administrateur systèmes dans le Sud de la France. Il maintient par ailleurs une vingtaine de modules sur le CPAN et essaye d'aider au développement de Perl en fournissant des services comme Perl cover et un Gonzui du CPAN.
Philippe Blayo - Paris
Philippe Blayo est davantage développeur. Il a mis en place des tests depuis 2001 sur des projets en Perl et dans d'autres langages.
Merci aux Mongueurs qui ont assuré la relecture de cet article.
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.