Was macht Perl-Testmuster?
Umfassende Teststrategien für Perl-Anwendungen mit Test2::V0, Test::More, prove und TDD-Methodik.
Wann aktivieren
- Schreiben von neuem Perl-Code (TDD befolgen: rot, grün, refaktorieren)
- Entwerfen von Testsuiten für Perl-Module oder -Anwendungen
- Überprüfen der Perl-Testabdeckung
- Einrichten der Perl-Testinfrastruktur
- Migration von Tests von Test::More zu Test2::V0
- Debuggen fehlschlagender Perl-Tests
TDD-Workflow
Befolge immer den ROT-GRÜN-REFACTOR-Zyklus.
# Schritt 1: ROT — Einen fehlschlagenden Test schreiben
# 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, 'addiert zwei Zahlen');
is($calc->add(-1, 1), 0, 'behandelt negative Zahlen');
};
done_testing;
# Schritt 2: GRÜN — Minimale Implementierung schreiben
# lib/Calculator.pm
package Calculator;
use v5.36;
use Moo;
sub add($self, $a, $b) {
return $a + $b;
}
1;
# Schritt 3: REFACTOR — Verbessern, während die Tests grün bleiben
# Ausführen: prove -lv t/unit/calculator.t
Test::More Grundlagen
Das Standard-Perl-Testmodul – weit verbreitet, wird mit dem Kern ausgeliefert.
Grundlegende Assertions
use v5.36;
use Test::More;
# Plan im Voraus oder done_testing verwenden
# plan tests => 5; # Fester Plan (optional)
# Gleichheit
is($result, 42, 'gibt korrekten Wert zurück');
isnt($result, 0, 'nicht null');
# Boolesch
ok($user->is_active, 'Benutzer ist aktiv');
ok(!$user->is_banned, 'Benutzer ist nicht gesperrt');
# Tiefenvergleich
is_deeply(
$got,
{ name => 'Alice', roles => ['admin'] },
'gibt erwartete Struktur zurück'
);
# Mustervergleich
like($error, qr/nicht gefunden/i, 'Fehler erwähnt "nicht gefunden"');
unlike($output, qr/passwort/, 'Ausgabe versteckt Passwort');
# Typprüfung
isa_ok($obj, 'MyApp::User');
can_ok($obj, 'save', 'delete');
done_testing;
SKIP und TODO
use v5.36;
use Test::More;
# Tests bedingt überspringen
SKIP: {
skip 'Keine Datenbank konfiguriert', 2 unless $ENV{TEST_DB};
my $db = connect_db();
ok($db->ping, 'Datenbank ist erreichbar');
is($db->version, '15', 'korrekte PostgreSQL-Version');
}
# Erwartete Fehlschläge markieren
TODO: {
local $TODO = 'Caching noch nicht implementiert';
is($cache->get('key'), 'value', 'Cache gibt Wert zurück');
}
done_testing;
Test2::V0 Modernes Framework
Test2::V0 ist der moderne Ersatz für Test::More – reichhaltigere Assertions, bessere Diagnostik und erweiterbar.
Warum Test2?
- Hervorragender Tiefenvergleich mit Hash-/Array-Buildern
- Bessere Diagnoseausgabe bei Fehlschlägen
- Subtests mit saubererem Gültigkeitsbereich
- Erweiterbar über Test2::Tools::*-Plugins
- Abwärtskompatibel mit Test::More-Tests
Tiefenvergleich mit Buildern
use v5.36;
use Test2::V0;
# Hash-Builder — Teilweise Struktur prüfen
is(
$user->to_hash,
hash {
field name => 'Alice';
field email => match(qr/\@example\.com$/);
field age => validator(sub { $_ >= 18 });
# Andere Felder ignorieren
etc();
},
'Benutzer hat erwartete Felder'
);
# Array-Builder
is(
$result,
array {
item 'erster';
item match(qr/^zweiter/);
item DNE(); # Does Not Exist — überprüft, dass keine zusätzlichen Elemente vorhanden sind
},
'Ergebnis entspricht der erwarteten Liste'
);
# Bag — Reihenfolge-unabhängiger Vergleich
is(
$tags,
bag {
item 'perl';
item 'testing';
item 'tdd';
},
'hat alle erforderlichen Tags, unabhängig von der Reihenfolge'
);
Subtests
use v5.36;
use Test2::V0;
subtest 'Benutzererstellung' => sub {
my $user = User->new(name => 'Alice', email => '[email protected]');
ok($user, 'Benutzerobjekt erstellt');
is($user->name, 'Alice', 'Name gesetzt');
is($user->email, '[email protected]', 'E-Mail gesetzt');
};
subtest 'Benutzervalidierung' => sub {
my $warnings = warns {
User->new(name => '', email => 'schlecht');
};
ok($warnings, 'warnt bei ungültigen Daten');
};
done_testing;
Exception-Tests mit Test2
use v5.36;
use Test2::V0;
# Testen, dass Code stirbt
like(
dies { divide(10, 0) },
qr/Division durch Null/,
'stirbt bei Division durch Null'
);
# Testen, dass Code lebt
ok(lives { divide(10, 2) }, 'Division erfolgreich') or note($@);
# Kombiniertes Muster
subtest 'Fehlerbehandlung' => sub {
ok(lives { parse_config('gueltig.json') }, 'gültige Konfiguration wird geparst');
like(
dies { parse_config('fehlt.json') },
qr/Kann nicht öffnen/,
'fehlende Datei stirbt mit Nachricht'
);
};
done_testing;
Testorganisation und prove
Verzeichnisstruktur
t/
├── 00-load.t # Überprüfen, ob Module kompilieren
├── 01-basic.t # Kernfunktionalität
├── unit/
│ ├── config.t # Unit-Tests pro Modul
│ ├── user.t
│ └── util.t
├── integration/
│ ├── database.t
│ └── api.t
├── lib/
│ └── TestHelper.pm # Gemeinsame Test-Hilfsfunktionen
└── fixtures/
├── config.json # Testdatendateien
└── users.csv
prove-Befehle
# Alle Tests ausführen
prove -l t/
# Ausführliche Ausgabe
prove -lv t/
# Bestimmten Test ausführen
prove -lv t/unit/user.t
# Rekursive Suche
prove -lr t/
# Parallele Ausführung (8 Jobs)
prove -lr -j8 t/
# Nur fehlgeschlagene Tests aus dem letzten Durchlauf ausführen
prove -l --state=failed t/
# Farbige Ausgabe mit Timer
prove -l --color --timer t/
# TAP-Ausgabe für CI
prove -l --formatter TAP::Formatter::JUnit t/ > results.xml
.proverc-Konfiguration
-l
--color
--timer
-r
-j4
--state=save
Testdaten und Setup/Teardown
Subtest-Isolation
use v5.36;
use Test2::V0;
use File::Temp qw(tempdir);
use Path::Tiny;
subtest 'Dateiverarbeitung' => sub {
# Setup
my $dir = tempdir(CLEANUP => 1);
my $file = path($dir, 'eingabe.txt');
$file->spew_utf8("zeile1\nzeile2\nzeile3\n");
# Test
my $result = process_file("$file");
is($result->{line_count}, 3, 'zählt Zeilen');
# Teardown erfolgt automatisch (CLEANUP => 1)
};
Gemeinsame Test-Hilfsfunktionen
Platziere wiederverwendbare Helfer in t/lib/TestHelper.pm und lade sie mit use lib 't/lib'. Exportiere Factory-Funktionen wie create_test_db(), create_temp_dir() und fixture_path() über Exporter.
Mocking
Test::MockModule
use v5.36;
use Test2::V0;
use Test::MockModule;
subtest 'externe API mocken' => sub {
my $mock = Test::MockModule->new('MyApp::API');
# Gut: Mock gibt kontrollierte Daten zurück
$mock->mock(fetch_user => sub ($self, $id) {
return { id => $id, name => 'Mock-Benutzer', email => '[email protected]' };
});
my $api = MyApp::API->new;
my $user = $api->fetch_user(42);
is($user->{name}, 'Mock-Benutzer', 'gibt gemockten Benutzer zurück');
# Aufrufanzahl überprüfen
my $call_count = 0;
$mock->mock(fetch_user => sub { $call_count++; return {} });
$api->fetch_user(1);
$api->fetch_user(2);
is($call_count, 2, 'fetch_user zweimal aufgerufen');
# Mock wird automatisch wiederhergestellt, wenn $mock den Gültigkeitsbereich verlässt
};
# Schlecht: Affen-Patching ohne Wiederherstellung
# *MyApp::API::fetch_user = sub { ... }; # NIEMALS — verteilt sich auf andere Tests
Für leichtgewichtige Mock-Objekte verwende Test::MockObject, um injizierbare Test-Doubles mit ->mock() zu erstellen und Aufrufe mit ->called_ok() zu überprüfen.
Abdeckung mit Devel::Cover
Abdeckung messen
# Basis-Abdeckungsbericht
cover -test
# Oder Schritt für Schritt
perl -MDevel::Cover -Ilib t/unit/user.t
cover
# HTML-Bericht
cover -report html
open cover_db/coverage.html
# Bestimmte Schwellwerte
cover -test -report text | grep 'Total'
# CI-freundlich: Fehlschlag unter Schwellwert
cover -test && cover -report text -select '^lib/' \
| perl -ne 'if (/Total.*?(\d+\.\d+)/) { exit 1 if $1 < 80 }'
Integrationstests
Verwende In-Memory-SQLite für Datenbanktests, mocke HTTP::Tiny für API-Tests.
use v5.36;
use Test2::V0;
use DBI;
subtest 'Datenbankintegration' => sub {
my $dbh = DBI->connect('dbi:SQLite:dbname=:memory:', '', '', {
RaiseError => 1,
});
$dbh->do('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
$dbh->prepare('INSERT INTO users (name) VALUES (?)')->execute('Alice');
my $row = $dbh->selectrow_hashref('SELECT * FROM users WHERE name = ?', undef, 'Alice');
is($row->{name}, 'Alice', 'Benutzer eingefügt und abgerufen');
};
done_testing;
Best Practices
MACHEN
- TDD befolgen: Schreibe Tests vor der Implementierung (rot-grün-refaktorieren)
- Test2::V0 verwenden: Moderne Assertions, bessere Diagnostik
- Subtests verwenden: Gruppiere zusammengehörige Assertions, isoliere Zustand
- Externe Abhängigkeiten mocken: Netzwerk, Datenbank, Dateisystem
prove -lverwenden: lib/ immer in@INCeinbeziehen- Tests klar benennen:
'Benutzeranmeldung mit ungültigem Passwort schlägt fehl' - Randfälle testen: Leere Zeichenketten, undef, Null, Grenzwerte
- 80%+ Abdeckung anstreben: Fokus auf Geschäftslogikpfade
- Tests schnell halten: I/O mocken, In-Memory-Datenbanken verwenden
NICHT MACHEN
- Nicht die Implementierung testen: Verhalten und Ausgabe testen, nicht Interna
- Keinen Zustand zwischen Subtests teilen: Jeder Subtest sollte unabhängig sein
done_testingnicht auslassen: Stellt sicher, dass alle geplanten Tests ausgeführt wurden- Nicht übermäßig mocken: Nur Grenzen mocken, nicht den Code unter Test
Test::Morenicht für neue Projekte verwenden: Test2::V0 bevorzugen- Testfehler nicht ignorieren: Alle Tests müssen vor dem Mergen bestehen
- CPAN-Module nicht testen: Vertraue darauf, dass die Bibliotheken korrekt funktionieren
- Keine zerbrechlichen Tests schreiben: Zu spezifischen String-Vergleich vermeiden
Kurzreferenz
| Aufgabe | Befehl / Muster |
|---|---|
| Alle Tests ausführen | prove -lr t/ |
| Einen Test ausführlich ausführen | prove -lv t/unit/user.t |
| Parallele Testausführung | prove -lr -j8 t/ |
| Abdeckungsbericht | cover -test && cover -report html |
| Gleichheit testen | is($got, $expected, 'label') |
| Tiefenvergleich | is($got, hash { field k => 'v'; etc() }, 'label') |
| Exception testen | like(dies { ... }, qr/msg/, 'label') |
| Keine Exception testen | ok(lives { ... }, 'label') |
| Methode mocken | Test::MockModule->new('Pkg')->mock(m => sub { ... }) |
| Tests überspringen | SKIP: { skip 'grund', $anzahl unless $bedingung; ... } |
| TODO-Tests | TODO: { local $TODO = 'grund'; ... } |
Häufige Fallstricke
done_testing vergessen
# Schlecht: Testdatei läuft, überprüft aber nicht, ob alle Tests ausgeführt wurden
use Test2::V0;
is(1, 1, 'funktioniert');
# done_testing fehlt — stille Fehler, wenn Code übersprungen wird
# Gut: Immer mit done_testing beenden
use Test2::V0;
is(1, 1, 'funktioniert');
done_testing;
Fehlendes -l-Flag
# Schlecht: Module in lib/ werden nicht gefunden
prove t/unit/user.t
# Kann MyApp/User.pm nicht in @INC finden
# Gut: lib/ in @INC einbeziehen
prove -l t/unit/user.t
Übermäßiges Mocken
Mocke die Abhängigkeit, nicht den Code unter Test. Wenn dein Test nur überprüft, ob ein Mock das zurückgibt, was du ihm gesagt hast, testet er nichts.
Testverschmutzung
Verwende my-Variablen innerhalb von Subtests – niemals our – um zu verhindern, dass Zustand zwischen Tests ausläuft.
Denk dran: Tests sind dein Sicherheitsnetz. Halte sie schnell, fokussiert und unabhängig. Verwende Test2::V0 für neue Projekte, prove zum Ausführen und Devel::Cover für die Rechenschaftspflicht.