[couverture de Linux Magazine 88]

Les tests en Perl - Présentation et modules standards

Article publié dans Linux Magazine 88, novembre 2006.

Copyright © 2006 - Sébastien Aperghis-Tramoni, Philippe Blayo

[+ del.icio.us] [+ Developers Zone] [+ Bookmarks.fr] [Digg this] [+ My Yahoo!]

Chapeau de l'article

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.

Des tests ? Pour quoi faire ?

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 :

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 ?

Éviter les régressions

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.

Trouver plus vite l'origine d'un problème

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.

Empêcher la réapparition d'un bug

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.

Décrire le comportement d'un module ou d'un programme

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.

Bug compatible ?

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.

Mesurer l'avancement d'un travail

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.

Eléments de nomenclature

On croise beaucoup d'appellations différentes, qui se recouvrent plus ou moins. On en citera deux :

Tests développeur / tests de recette

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.

Tests unitaires / tests applicatifs

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).

Le protocole de test en Perl

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.

TAP, Test Anything Protocol

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

[couverture du livre]

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.

Séance de dissection

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 :

Les directives permettent de modifier l'interprétation de l'état d'un test. Les deux directives supportées sont :

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].

Et en pratique ?

É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 générateur de TAP

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.

L'intérêt de TAP

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.

Test::More

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.

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.

Quand utiliser 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.

Disponibilité

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.

Test::Harness, l'interpréteur de résultats

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.

Exécution des scripts de test

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/.

Conclusion

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.

Références

[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

Auteurs

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.

[IE7, par Dean Edwards] [Validation du HTML] [Validation du CSS]