Modèles de Tests Perl
Stratégies de test complètes pour les applications Perl utilisant Test2::V0, Test::More, prove, et la méthodologie TDD.
Quand Activer
- Écrire du nouveau code Perl (suivre le TDD : rouge, vert, refactoriser)
- Concevoir des suites de tests pour des modules ou applications Perl
- Réviser la couverture de test Perl
- Mettre en place une infrastructure de test Perl
- Migrer des tests de Test::More vers Test2::V0
- Déboguer des tests Perl qui échouent
Workflow TDD
Toujours suivre le cycle ROUGE-VERT-REFACTORISER.
# Étape 1 : ROUGE — Écrire un test qui échoue
# t/unit/calculator.t
use v5.36;
use Test2::V0;
use lib 'lib';
use Calculator;
subtest 'addition' => sub {
my $calc = Calculator->new;
is($calc->add(2, 3), 5, 'additionne deux nombres');
is($calc->add(-1, 1), 0, 'gère les nombres négatifs');
};
done_testing;
# Étape 2 : VERT — Écrire l'implémentation minimale
# lib/Calculator.pm
package Calculator;
use v5.36;
use Moo;
sub add($self, $a, $b) {
return $a + $b;
}
1;
# Étape 3 : REFACTORISER — Améliorer tout en gardant les tests verts
# Exécuter : prove -lv t/unit/calculator.t
Fondamentaux de Test::More
Le module de test Perl standard — largement utilisé, fourni avec le cœur de Perl.
Assertions de Base
use v5.36;
use Test::More;
# Planifier à l'avance ou utiliser done_testing
# plan tests => 5; # Plan fixe (optionnel)
# Égalité
is($resultat, 42, 'retourne la bonne valeur');
isnt($resultat, 0, 'pas zéro');
# Booléen
ok($utilisateur->is_active, 'l\'utilisateur est actif');
ok(!$utilisateur->is_banned, 'l\'utilisateur n\'est pas banni');
# Comparaison profonde
is_deeply(
$obtenu,
{ nom => 'Alice', roles => ['admin'] },
'retourne la structure attendue'
);
# Correspondance de motif
like($erreur, qr/introuvable/i, 'l\'erreur mentionne introuvable');
unlike($sortie, qr/mot_de_passe/, 'la sortie cache le mot de passe');
# Vérification de type
isa_ok($obj, 'MonApp::Utilisateur');
can_ok($obj, 'sauvegarder', 'supprimer');
done_testing;
SKIP et TODO
use v5.36;
use Test::More;
# Sauter des tests conditionnellement
SKIP: {
skip 'Aucune base de données configurée', 2 à moins que $ENV{TEST_DB};
my $db = connect_db();
ok($db->ping, 'la base de données est accessible');
is($db->version, '15', 'version PostgreSQL correcte');
}
# Marquer des échecs attendus
TODO: {
local $TODO = 'Cache non encore implémenté';
is($cache->get('clé'), 'valeur', 'le cache retourne la valeur');
}
done_testing;
Framework Moderne Test2::V0
Test2::V0 est le remplacement moderne de Test::More — assertions plus riches, meilleurs diagnostics et extensible.
Pourquoi Test2 ?
- Comparaison profonde supérieure avec les constructeurs de hachages/tableaux
- Meilleure sortie diagnostique en cas d'échec
- Sous-tests avec une portée plus propre
- Extensible via les plugins Test2::Tools::*
- Rétrocompatible avec les tests Test::More
Comparaison Profonde avec les Constructeurs
use v5.36;
use Test2::V0;
# Constructeur de hachage — vérifier une structure partielle
is(
$utilisateur->to_hash,
hash {
field nom => 'Alice';
field email => match(qr/\@exemple\.com$/);
field age => validator(sub { $_ >= 18 });
# Ignorer les autres champs
etc();
},
'l\'utilisateur a les champs attendus'
);
# Constructeur de tableau
is(
$resultat,
array {
item 'premier';
item match(qr/^second/);
item DNE(); # N'existe pas — vérifier aucun élément supplémentaire
},
'le résultat correspond à la liste attendue'
);
# Bag — comparaison indépendante de l'ordre
is(
$etiquettes,
bag {
item 'perl';
item 'test';
item 'tdd';
},
'a toutes les étiquettes requises, quel que soit l\'ordre'
);
Sous-tests
use v5.36;
use Test2::V0;
subtest 'Création d\'utilisateur' => sub {
my $utilisateur = User->new(nom => 'Alice', email => '[email protected]');
ok($utilisateur, 'objet utilisateur créé');
is($utilisateur->nom, 'Alice', 'le nom est défini');
is($utilisateur->email, '[email protected]', 'l\'email est défini');
};
subtest 'Validation d\'utilisateur' => sub {
my $avertissements = warns {
User->new(nom => '', email => 'mauvais');
};
ok($avertissements, 'avertit sur des données invalides');
};
done_testing;
Test d'Exceptions avec Test2
use v5.36;
use Test2::V0;
# Tester que le code meurt
like(
dies { diviser(10, 0) },
qr/Division par zéro/,
'meurt sur une division par zéro'
);
# Tester que le code vit
ok(lives { diviser(10, 2) }, 'la division réussit') ou note($@);
# Motif combiné
subtest 'gestion des erreurs' => sub {
ok(lives { parse_config('valide.json') }, 'la configuration valide est analysée');
like(
dies { parse_config('manquant.json') },
qr/Impossible d\'ouvrir/,
'le fichier manquant meurt avec un message'
);
};
done_testing;
Organisation des Tests et prove
Structure de Répertoire
t/
├── 00-chargement.t # Vérifier que les modules se compilent
├── 01-base.t # Fonctionnalités de base
├── unit/
│ ├── config.t # Tests unitaires par module
│ ├── utilisateur.t
│ └── util.t
├── integration/
│ ├── base_de_donnees.t
│ └── api.t
├── lib/
│ └── AideTest.pm # Utilitaires de test partagés
└── fixtures/
├── config.json # Fichiers de données de test
└── utilisateurs.csv
Commandes prove
# Exécuter tous les tests
prove -l t/
# Sortie verbeuse
prove -lv t/
# Exécuter un test spécifique
prove -lv t/unit/utilisateur.t
# Recherche récursive
prove -lr t/
# Exécution parallèle (8 tâches)
prove -lr -j8 t/
# Exécuter uniquement les tests ayant échoué lors de la dernière exécution
prove -l --state=failed t/
# Sortie colorée avec minuteur
prove -l --color --timer t/
# Sortie TAP pour CI
prove -l --formatter TAP::Formatter::JUnit t/ > resultats.xml
Configuration .proverc
-l
--color
--timer
-r
-j4
--state=save
Fixtures et Configuration/Nettoyage
Isolation des Sous-tests
use v5.36;
use Test2::V0;
use File::Temp qw(tempdir);
use Path::Tiny;
subtest 'traitement de fichier' => sub {
# Configuration
my $repertoire = tempdir(CLEANUP => 1);
my $fichier = path($repertoire, 'entree.txt');
$fichier->spew_utf8("ligne1\nligne2\nligne3\n");
# Test
my $resultat = traiter_fichier("$fichier");
is($resultat->{nombre_lignes}, 3, 'compte les lignes');
# Le nettoyage se fait automatiquement (CLEANUP => 1)
};
Aides de Test Partagés
Placez les aides réutilisables dans t/lib/AideTest.pm et chargez-les avec use lib 't/lib'. Exportez des fonctions de fabrique comme creer_bd_test(), creer_repertoire_temp(), et chemin_fixture() via Exporter.
Mocking
Test::MockModule
use v5.36;
use Test2::V0;
use Test::MockModule;
subtest 'moquer une API externe' => sub {
my $mock = Test::MockModule->new('MonApp::API');
# Bien : Mock retourne des données contrôlées
$mock->mock(recuperer_utilisateur => sub ($self, $id) {
return { id => $id, nom => 'Utilisateur Mock', email => '[email protected]' };
});
my $api = MonApp::API->new;
my $utilisateur = $api->recuperer_utilisateur(42);
is($utilisateur->{nom}, 'Utilisateur Mock', 'retourne l\'utilisateur mocké');
# Vérifier le nombre d'appels
my $nombre_appels = 0;
$mock->mock(recuperer_utilisateur => sub { $nombre_appels++; return {} });
$api->recuperer_utilisateur(1);
$api->recuperer_utilisateur(2);
is($nombre_appels, 2, 'recuperer_utilisateur appelé deux fois');
# Le mock est automatiquement restauré quand $mock sort de la portée
};
# Mauvais : Monkey-patching sans restauration
# *MonApp::API::recuperer_utilisateur = sub { ... }; # JAMAIS — fuit entre les tests
Pour des objets mock légers, utilisez Test::MockObject pour créer des doubles de test injectables avec ->mock() et vérifier les appels avec ->called_ok().
Couverture avec Devel::Cover
Exécuter la Couverture
# Rapport de couverture de base
cover -test
# Ou étape par étape
perl -MDevel::Cover -Ilib t/unit/utilisateur.t
cover
# Rapport HTML
cover -report html
ouvrir cover_db/coverage.html
# Seuils spécifiques
cover -test -report text | grep 'Total'
# Compatible CI : échouer en dessous d'un seuil
cover -test && cover -report text -select '^lib/' \
| perl -ne 'if (/Total.*?(\d+\.\d+)/) { exit 1 if $1 < 80 }'
Tests d'Intégration
Utilisez SQLite en mémoire pour les tests de base de données, et moquez HTTP::Tiny pour les tests d'API.
use v5.36;
use Test2::V0;
use DBI;
subtest 'intégration base de données' => sub {
my $dbh = DBI->connect('dbi:SQLite:dbname=:memory:', '', '', {
RaiseError => 1,
});
$dbh->do('CREATE TABLE utilisateurs (id INTEGER PRIMARY KEY, nom TEXT)');
$dbh->prepare('INSERT INTO utilisateurs (nom) VALUES (?)')->execute('Alice');
my $ligne = $dbh->selectrow_hashref('SELECT * FROM utilisateurs WHERE nom = ?', undef, 'Alice');
is($ligne->{nom}, 'Alice', 'utilisateur inséré et récupéré');
};
done_testing;
Bonnes Pratiques
À FAIRE
- Suivre le TDD : Écrire les tests avant l'implémentation (rouge-vert-refactoriser)
- Utiliser Test2::V0 : Assertions modernes, meilleurs diagnostics
- Utiliser les sous-tests : Grouper les assertions liées, isoler l'état
- Moquer les dépendances externes : Réseau, base de données, système de fichiers
- Utiliser
prove -l: Toujours inclure lib/ dans@INC - Nommer les tests clairement :
'connexion utilisateur avec mot de passe invalide échoue' - Tester les cas limites : Chaînes vides, undef, zéro, valeurs aux limites
- Viser 80 %+ de couverture : Se concentrer sur les chemins métier
- Garder les tests rapides : Moquer les E/S, utiliser des bases de données en mémoire
À NE PAS FAIRE
- Ne pas tester l'implémentation : Tester le comportement et le résultat, pas les détails internes
- Ne pas partager l'état entre les sous-tests : Chaque sous-test doit être indépendant
- Ne pas omettre
done_testing: Garantit que tous les tests planifiés ont été exécutés - Ne pas trop moquer : Moquer uniquement les frontières, pas le code testé
- Ne pas utiliser
Test::Morepour les nouveaux projets : Préférer Test2::V0 - Ne pas ignorer les échecs de test : Tous les tests doivent passer avant la fusion
- Ne pas tester les modules CPAN : Faire confiance aux bibliothèques
- Ne pas écrire de tests fragiles : Éviter la correspondance de chaîne trop spécifique
Référence Rapide
| Tâche | Commande / Modèle |
|---|---|
| Exécuter tous les tests | prove -lr t/ |
| Exécuter un test en verbeux | prove -lv t/unit/utilisateur.t |
| Exécution parallèle | prove -lr -j8 t/ |
| Rapport de couverture | cover -test && cover -report html |
| Tester l'égalité | is($obtenu, $attendu, 'label') |
| Comparaison profonde | is($obtenu, hash { field k => 'v'; etc() }, 'label') |
| Tester une exception | like(dies { ... }, qr/msg/, 'label') |
| Tester aucune exception | ok(lives { ... }, 'label') |
| Moquer une méthode | Test::MockModule->new('Pkg')->mock(m => sub { ... }) |
| Sauter des tests | SKIP: { skip 'raison', $compte à moins que $condition; ... } |
| Tests TODO | TODO: { local $TODO = 'raison'; ... } |
Pièges Courants
Oublier done_testing
# Mauvais : Le fichier de test s'exécute mais ne vérifie pas que tous les tests ont été exécutés
use Test2::V0;
is(1, 1, 'fonctionne');
# done_testing manquant — bugs silencieux si le code de test est sauté
# Bien : Toujours terminer par done_testing
use Test2::V0;
is(1, 1, 'fonctionne');
done_testing;
Drapeau -l Manquant
# Mauvais : Modules dans lib/ non trouvés
prove t/unit/utilisateur.t
# Impossible de localiser MonApp/Utilisateur.pm dans @INC
# Bien : Inclure lib/ dans @INC
prove -l t/unit/utilisateur.t
Sur-Moquage
Moquez la dépendance, pas le code testé. Si votre test vérifie seulement qu'un mock retourne ce que vous lui avez dit, il ne teste rien.
Pollution de Test
Utilisez des variables my à l'intérieur des sous-tests — jamais our — pour empêcher que l'état ne fuite entre les tests.
Rappel : Les tests sont votre filet de sécurité. Gardez-les rapides, ciblés et indépendants. Utilisez Test2::V0 pour les nouveaux projets, prove pour l'exécution, et Devel::Cover pour la responsabilité.