Tests en Perl¶
L'écosystème de test Perl repose sur le format TAP (Test Anything Protocol), un protocole texte simple qui permet l'interopérabilité entre frameworks et runners. Test::More et Test2::V0 sont les frameworks les plus utilisés, tandis que prove orchestre l'exécution et Devel::Cover mesure la couverture de code.
Comparatif des outils de test¶
| Outil | Rôle | Points forts | Usage typique |
|---|---|---|---|
| Test::More | Framework de base | Ubiquite, compatibilité universelle, docs abondantes | Tests unitaires classiques |
| Test2::V0 | Framework moderne | Meilleure gestion des erreurs, API claire | Nouveaux projets |
| Test::Harness | Runner TAP | Aggregation des résultats, rapport détaillé | CI/CD, suites de tests |
| prove | CLI de test | Parallèle, couleurs, .proverc configurable | Ligne de commande locale |
| Test::Mojo | Test d'applications Mojo | Requêtes HTTP simulees, assertions JSON/HTML | Tests d'intégration Mojolicious |
| Test::MockModule | Mocking de modules | Remplacement temporaire de sous-routines | Isolation des dépendances |
| Devel::Cover | Couverture de code | Rapports HTML, line/branch/condition coverage | Mesure de qualité |
Configuration prove et .proverc¶
# Installation des outils de test
cpanm Test::More Test2::V0 Test::Mojo Test::MockModule Devel::Cover
# .proverc — configuration du runner prove
# Place a la racine du projet
# Executer en parallele sur 4 workers
--jobs 4
# Affichage verbeux
--verbose
# Recursion dans le dossier t/
--recurse
# Format TAP colore
--color
# Dossiers de bibliotheques locales
--lib
--blib
# Lancement des tests
prove t/ # Tous les tests dans t/
prove t/unit/ t/integration/ # Dossiers specifiques
prove -v t/api.t # Un seul fichier en mode verbeux
prove -j4 t/ # Parallele (4 workers)
prove --timer t/ # Affiche la duree de chaque test
Tests unitaires avec Test::More¶
Test::More est le module de test standard, disponible dans le core Perl depuis la version 5.8.
#!/usr/bin/perl
# t/unit/item_model.t — Tests unitaires du modele Item
use strict;
use warnings;
use Test::More;
use DBI;
# Chargement du module a tester
use lib 'lib';
use MojoCRUD::Model::Item;
# --- Setup : base de donnees en memoire ---
my $dbh = DBI->connect(
'dbi:SQLite:dbname=:memory:',
undef, undef,
{ RaiseError => 1, AutoCommit => 1 }
);
my $model = MojoCRUD::Model::Item->new($dbh);
# --- Tests de creation ---
subtest 'creation d\'un item' => sub {
my $id = $model->creer({ nom => 'Widget', description => 'Un widget', quantite => 5 });
ok(defined $id, 'creer() retourne un id');
ok($id > 0, 'l\'id est positif');
my $item = $model->par_id($id);
is($item->{nom}, 'Widget', 'nom correct');
is($item->{quantite}, 5, 'quantite correcte');
is($item->{description}, 'Un widget', 'description correcte');
};
# --- Tests de liste ---
subtest 'liste des items' => sub {
# Ajout de deux items supplementaires
$model->creer({ nom => 'Gadget', quantite => 10 });
$model->creer({ nom => 'Truc', quantite => 3 });
my $items = $model->liste;
ok(ref($items) eq 'ARRAY', 'liste() retourne un arrayref');
cmp_ok(scalar(@$items), '>=', 3, 'au moins 3 items dans la liste');
};
# --- Tests de mise a jour ---
subtest 'mise a jour d\'un item' => sub {
my $id = $model->creer({ nom => 'Original', quantite => 1 });
my $lignes = $model->mettre_a_jour($id, {
nom => 'Modifie',
quantite => 99,
description => 'Nouveau',
});
is($lignes, 1, 'une ligne mise a jour');
my $item = $model->par_id($id);
is($item->{nom}, 'Modifie', 'nom mis a jour');
is($item->{quantite}, 99, 'quantite mise a jour');
};
# --- Tests de suppression ---
subtest 'suppression d\'un item' => sub {
my $id = $model->creer({ nom => 'A supprimer', quantite => 0 });
my $lignes = $model->supprimer($id);
is($lignes, 1, 'une ligne supprimee');
my $item = $model->par_id($id);
ok(!defined $item, 'item introuvable apres suppression');
};
# --- Tests des cas limites ---
subtest 'cas limites' => sub {
my $item = $model->par_id(99999);
ok(!defined $item, 'par_id() retourne undef pour un id inexistant');
my $lignes = $model->supprimer(99999);
is($lignes, 0, 'supprimer() retourne 0 pour un id inexistant');
};
done_testing;
Tests d'intégration avec Test::Mojo¶
Test::Mojo permet de tester une application Mojolicious sans serveur HTTP réel, en envoyant des requêtes directement à l'application PSGI.
#!/usr/bin/perl
# t/integration/api.t — Tests de l'API REST Mojolicious
use strict;
use warnings;
use Test::More;
use Test::Mojo;
# Charge l'application en mode test
use lib 'lib';
$ENV{MOJO_MODE} = 'test';
# Utilise une base SQLite en memoire pour les tests
$ENV{DATABASE_URL} = 'dbi:SQLite:dbname=:memory:';
my $t = Test::Mojo->new('app.pl'); # ou le nom de la classe app
# --- Test GET /api/items (liste vide) ---
$t->get_ok('/api/items')
->status_is(200)
->json_is([]); # tableau vide attendu
# --- Test POST /api/items ---
$t->post_ok('/api/items' => json => {
nom => 'Clavier',
quantite => 10,
})
->status_is(201)
->json_has('/id')
->json_is('/nom' => 'Clavier')
->json_is('/quantite' => 10);
# Recuperation de l'id cree
my $id = $t->tx->res->json('/id');
ok(defined $id && $id > 0, "id cree : $id");
# --- Test GET /api/items/:id ---
$t->get_ok("/api/items/$id")
->status_is(200)
->json_is('/nom' => 'Clavier');
# --- Test PUT /api/items/:id ---
$t->put_ok("/api/items/$id" => json => {
nom => 'Clavier Mecanique',
quantite => 5,
})
->status_is(200)
->json_is('/nom' => 'Clavier Mecanique')
->json_is('/quantite' => 5);
# --- Test DELETE /api/items/:id ---
$t->delete_ok("/api/items/$id")
->status_is(204);
# Verification que l'item n'existe plus
$t->get_ok("/api/items/$id")
->status_is(404)
->json_has('/erreur');
# --- Test de validation ---
$t->post_ok('/api/items' => json => { quantite => 5 }) # nom manquant
->status_is(400)
->json_has('/erreurs');
done_testing;
Mode test Mojolicious
Definissez $ENV{MOJO_MODE} = 'test' avant de créer l'objet Test::Mojo. Mojolicious charge alors le fichier de configuration app.test.conf s'il existe, permettant de séparer la configuration de test de la production.
Mocking avec Test::MockModule¶
Test::MockModule permet de remplacer temporairement des sous-routines dans un module, sans modifier le code de production.
#!/usr/bin/perl
# t/unit/service_email.t — Mocking d'un service externe
use strict;
use warnings;
use Test::More;
use Test::MockModule;
use lib 'lib';
use MonApp::Service::Email;
use MonApp::Service::Notification;
# --- Mocking du service email ---
subtest 'notification envoyee par email en cas de succes' => sub {
# On intercepte MonApp::Service::Email::envoyer
my $mock_email = Test::MockModule->new('MonApp::Service::Email');
my @emails_envoyes;
$mock_email->mock('envoyer', sub {
my ($self, %args) = @_;
push @emails_envoyes, \%args;
return 1; # Succes simule
});
my $notif = MonApp::Service::Notification->new;
$notif->notifier(
destinataire => 'alice@example.com',
sujet => 'Test',
message => 'Bonjour',
);
is(scalar @emails_envoyes, 1, 'un email envoye');
is($emails_envoyes[0]{destinataire}, 'alice@example.com', 'bon destinataire');
# Le mock est restaure automatiquement en sortie de scope
};
# --- Mocking d'une erreur reseau ---
subtest 'retry en cas d\'erreur reseau' => sub {
my $mock_email = Test::MockModule->new('MonApp::Service::Email');
my $tentatives = 0;
# Premier appel echoue, second reussit
$mock_email->mock('envoyer', sub {
$tentatives++;
die "Connexion refusee\n" if $tentatives == 1;
return 1;
});
my $notif = MonApp::Service::Notification->new;
my $ok = eval { $notif->notifier_avec_retry(
destinataire => 'bob@example.com',
sujet => 'Retry test',
message => 'Hello',
max_tentatives => 3,
)};
is($tentatives, 2, 'deux tentatives effectuees');
ok($ok, 'succes apres retry');
};
done_testing;
Structure TAP¶
TAP (Test Anything Protocol) est le format de sortie standard de tous les modules de test Perl. Comprendre TAP permet de lire les sorties brutes et d'intégrer n'importe quel outil.
# Sortie TAP typique d'un fichier de test
TAP version 14
1..5
ok 1 - connexion a la base de donnees
ok 2 - creation d'un item
ok 3 - lecture par id
not ok 4 - mise a jour - quantite
# Failed test 'mise a jour - quantite'
# at t/item_model.t line 67.
# got: '10'
# expected: '99'
ok 5 - suppression
# Tests run: 5
# Tests passed: 4
# Tests failed: 1
# Test2::V0 — alternative moderne avec meilleure gestion des erreurs
#!/usr/bin/perl
use strict;
use warnings;
use Test2::V0;
use lib 'lib';
use MojoCRUD::Model::Item;
use DBI;
my $dbh = DBI->connect('dbi:SQLite:dbname=:memory:', undef, undef,
{ RaiseError => 1, AutoCommit => 1 });
my $model = MojoCRUD::Model::Item->new($dbh);
# Test2::V0 utilise is() pour les comparaisons profondes
my $id = $model->creer({ nom => 'Test', quantite => 1 });
my $item = $model->par_id($id);
# Comparaison profonde de structures
is($item, hash {
field nom => 'Test';
field quantite => 1;
etc(); # Ignore les champs supplementaires
}, 'item cree correctement');
# Verification qu'une exception est levee
like(
dies { $model->creer({}) }, # nom manquant -> erreur SQL
qr/NOT NULL constraint/,
'erreur si nom absent'
);
done_testing;
Couverture avec Devel::Cover¶
# Installation
cpanm Devel::Cover
# Lancement des tests avec instrumentation de couverture
cover -test
# Ou manuellement
PERL5OPT="-MDevel::Cover" prove t/
cover # Genere le rapport HTML dans cover_db/coverage.html
# Rapport dans le terminal
cover -report text
# Nettoyage de la base de couverture
cover -delete
# .coveragerc equivalent Perl : Makefile.PL ou fichier de config cover
# Options courantes lors de l'appel a cover
# Ignorer les fichiers de tests eux-memes
cover -ignore_re '^t/'
# Seuil minimum (echoue si inferieur)
cover -threshold 75
Couverture et code mort
Une couverture a 100% ne garantit pas l'absence de bugs. Privilegiez une couverture de branches (branch coverage) plutôt que simplement de lignes (statement coverage). Devel::Cover rapporte les deux avec le drapeau -coverage branch.
Intégration CI/CD¶
# .github/workflows/test.yml — Pipeline GitHub Actions
name: Tests Perl
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
perl: ['5.36', '5.38', '5.40']
steps:
- uses: actions/checkout@v4
- name: Installer Perl ${{ matrix.perl }}
uses: shogo82148/actions-setup-perl@v1
with:
perl-version: ${{ matrix.perl }}
- name: Installer les dependances
run: |
cpanm --notest --installdeps .
cpanm --notest Test::More Test::Mojo Test::MockModule Devel::Cover
- name: Lancer les tests
run: prove -r t/
- name: Rapport de couverture
run: |
cover -test -report text
cover -threshold 70