Was macht Perl-Testmuster?
Umfassende Teststrategien für Perl-Anwendungen mit Test2::V0, Test::More, prove und TDD-Methodik.
Wann aktivieren
- Beim Schreiben von neuem Perl-Code (TDD befolgen: rot, grün, refaktorisieren)
- Beim Entwerfen von Testsuiten für Perl-Module oder -Anwendungen
- Beim Überprüfen der Perl-Testabdeckung
- Beim Einrichten der Perl-Testinfrastruktur
- Beim Migrieren von Tests von Test::More zu Test2::V0
- Beim Debuggen fehlschlagender Perl-Tests
TDD-Workflow
Befolgen Sie immer den Rot-Grün-Refaktorisieren-Zyklus.
# Schritt 1: ROT – Schreiben Sie einen fehlschlagenden Test
# 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: REFAKTORISIEREN – Verbessern, während Tests grün bleiben
# Ausführen: prove -lv t/unit/calculator.t
Grundlagen von Test::More
Das Standard-Perl-Testmodul – weit verbreitet, im Core enthalten.
Grundlegende Assertions
use v5.36;
use Test::More;
# Plan vorher oder done_testing verwenden
# plan tests => 5; # Fester Plan (optional)
# Gleichheit
is($result, 42, 'gibt korrekten Wert zurück');
isnt($result, 0, 'nicht Null');
# Boolean
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');
# Typ-Prü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?
- Überlegener 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 – Teil-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 'first';
item match(qr/^second/);
item DNE(); # Does Not Exist – überprüft, dass keine zusätzlichen Elemente vorhanden
},
'Ergebnis entspricht erwarteter 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 ist gesetzt');
is($user->email, '[email protected]', 'E-Mail ist gesetzt');
};
subtest 'Benutzervalidierung' => sub {
my $warnings = warns {
User->new(name => '', email => 'bad');
};
ok($warnings, 'warnt bei ungültigen Daten');
};
done_testing;
Ausnahmetesting 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 Meldung'
);
};
done_testing;
Testorganisation und prove
Verzeichnisstruktur
t/
├── 00-load.t # Überprüft, 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-Hilfsprogramme
└── 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 Lauf 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
Fixtures 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-Helfer
Platzieren Sie wiederverwendbare Helfer in t/lib/TestHelper.pm und laden Sie sie mit use lib 't/lib'. Exportieren Sie 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 wurde zweimal aufgerufen');
# Mock wird automatisch wiederhergestellt, wenn $mock den Gültigkeitsbereich verlässt
};
# Schlecht: Monkey-Patching ohne Wiederherstellung
# *MyApp::API::fetch_user = sub { ... }; # NIEMALS – leckt über Tests hinweg
Für leichte Mock-Objekte verwenden Sie Test::MockObject, um injizierbare Test-Doubles mit ->mock() zu erstellen und Aufrufe mit ->called_ok() zu überprüfen.
Abdeckung mit Devel::Cover
Abdeckung ausführen
# 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 Schwellenwerte
cover -test -report text | grep 'Total'
# CI-freundlich: Fehlschlag bei Unterschreitung des Schwellenwerts
cover -test && cover -report text -select '^lib/' \
| perl -ne 'if (/Total.*?(\d+\.\d+)/) { exit 1 if $1 < 80 }'
Integrationstests
Verwenden Sie In-Memory-SQLite für Datenbanktests, mocken Sie 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;
Bewährte Vorgehensweisen
TUN
- TDD befolgen: Tests vor der Implementierung schreiben (rot-grün-refaktorisieren)
- Test2::V0 verwenden: Moderne Assertions, bessere Diagnostik
- Subtests verwenden: Zusammengehörige Assertions gruppieren, Zustand isolieren
- Externe Abhängigkeiten mocken: Netzwerk, Datenbank, Dateisystem
prove -lverwenden: Immer lib/ in@INCeinschließen- 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äftslogik-Pfade
- Tests schnell halten: I/O mocken, In-Memory-Datenbanken verwenden
NICHT TUN
- 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 gelaufen sind- Nicht übermäßig mocken: Nur Grenzen mocken, nicht den getesteten Code
Test::Morenicht für neue Projekte verwenden: Test2::V0 bevorzugen- Testfehlschläge nicht ignorieren: Alle Tests müssen vor dem Merge bestanden werden
- CPAN-Module nicht testen: Vertrauen Sie darauf, dass Bibliotheken korrekt funktionieren
- Keine zerbrechlichen Tests schreiben: Übermäßig spezifischen Zeichenkettenabgleich vermeiden
Kurzreferenz
| Aufgabe | Befehl / Muster |
|---|---|
| Alle Tests ausführen | prove -lr t/ |
| Einen Test ausführlich ausführen | prove -lv t/unit/user.t |
| Parallelen Testlauf | 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') |
| Ausnahme testen | like(dies { ... }, qr/msg/, 'label') |
| Keine Ausnahme 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, aber überprüft nicht, ob alle Tests ausgeführt wurden
use Test2::V0;
is(1, 1, 'funktioniert');
# Fehlendes done_testing – stille Fehler, wenn Testcode ü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
# Can't locate MyApp/User.pm in @INC
# Gut: lib/ in @INC einschließen
prove -l t/unit/user.t
Übermäßiges Mocken
Mocken Sie die Abhängigkeit, nicht den getesteten Code. Wenn Ihr Test nur überprüft, dass ein Mock das zurückgibt, was Sie ihm gesagt haben, testet er nichts.
Testverschmutzung
Verwenden Sie my-Variablen innerhalb von Subtests – niemals our – um zu verhindern, dass Zustand zwischen Tests durchsickert.
Denken Sie daran: Tests sind Ihr Sicherheitsnetz. Halten Sie sie schnell, fokussiert und unabhängig. Verwenden Sie Test2::V0 für neue Projekte, prove zum Ausführen und Devel::Cover für die Rechenschaftspflicht.