Eine Testsuite hilft Fehler zu korrigieren und Teile des Systems umzuschreiben, ohne die bestehende Codebasis dabei zu ruinieren. Die unbestechlichen Kontrolleure heißen etwa Test::More oder Test::Deep.
Auch bei der Weiterentwicklung und beim Refactoring stellt sich die Frage: Ist garantiert, dass ein Bugfix keine unerwünschten Nebenwirkungen hat? Wer ohne Aufwand ein paar hundert Tests ablaufen lassen kann, tauscht mit weniger Herzklopfen Teile des Systems aus und schläft ruhiger. Deshalb kommen selbst jene an einer Suite für Regressionstests nicht vorbei, die Bewegungen wie Extreme Programming für neumodischen Firlefanz halten.
Wenn ein Programm auf Anhieb funktioniert, macht es sich allein schon dadurch verdächtig – der Fall ist zu selten. Testgetriebene Entwicklung, die sich als Methode des Extreme Programming etabliert hat, baut gerade auf jenen unvermeidlichen Fehlern auf, die ständig erweiterte Testläufe finden. Ein Testfall geht schief, der Programmierer behebt das Problem – und prompt funktioniert das neu implementierte Feature.
Das motiviert nicht nur während der Entwicklung, jeder neue Test vergrößert auch die Suite, als deren Bestandteil er später wieder und wieder abläuft. So summieren sich kleine Schritte zu einem Gesamtwerk, das im Nachhinein keine Qualitätssicherungs-Abteilung der Welt zustande brächte.
Suite ist Standard
In der Perl-Community gehört es glücklicherweise zum guten Ton, einem CPAN-Modul eine Testsuite beizulegen, die alle wichtigen Funktionen überprüft. Wenn aber kein Modul, sondern nur ein kleines Skript entsteht? Über die Jahre habe ich festgestellt, dass ein Skript nur Kommandozeilenparameter verarbeiten sollte, die Hilfetexte ausspucken, und für alles Weitere Module verwenden, in die es alle anderen Funktionen packt. Von diesen Modulen profitieren später eventuell auch andere Skripte. Und selbstverständlich liegen ja jedem Modul eine Dokumentation und eine lückenlose Testsuite bei, oder etwa nicht?
Das TAP-Protokoll (Test Anything Protocol) hat sich in der Perl-Welt für Regressionstests durchgesetzt. Es gibt in der Regel zuerst eine Kopfzeile aus, die die Anzahl der folgenden Tests enthält. Darauf folgt je eine Zeile für jeden Testfall, die im Erfolgsfall »ok« und im Fehlerfall »not ok« lautet:
1..3 ok 1 not ok 2 ok 3
Bei Hunderten von Testfällen wäre die Ausgabe natürlich schwer zu überblicken. Darum kümmert sich eine darüber liegende Schicht um eine Zusammenfassung. In wenigen Zeilen spuckt sie aus, ob alles glatt ging oder wie viele Testfälle versagten. Da alle Testfälle ähnliche Zusammenhänge prüfen und Aktionen einleiten, gibt es spezielle Testmodule wie Test::More, die redundanten Code vermeiden helfen.
Auf Herz und Nieren
Traditionell haben Perl-Testskripte die Endung »*.t« und liegen im Verzeichnis »t« einer Modul-Distribution. Das Beispielskript »simple.t« (siehe Listing 1) testet das Modul Config::Patch vom CPAN, das Konfigurationsdateien flickt. Zuerst legt »Test::More« mit der Anweisung »tests => 4« fest, dass genau vier Testfälle ablaufen sollen. Das ist wichtig, denn falls die Testsuite unerwartet abbricht, sollte sie dies melden.
Weil die TestÂsuite gelegentlich schnell wächst, weichen manche Entwickler diese Prüfung mit »use Test::More qw(no_plan);« temporär auf, aber letztlich ist eine fest vorgegebene Testanzahl doch der bessere Weg.
|
Tabelle 1: |
|
|---|---|
|
Modul |
Funktion |
|
Test::Simple |
Gebräuchlichstes Testutility, enthält Test::More |
|
Test::Deep |
Vergleicht tief verschachtelte Strukturen |
|
Test::Pod |
Validiert POD-Dokumentation |
|
Test::Pod::Coverage |
Prüft, ob alle Funktionen dokumentiert sind |
|
Test::NoWarnings |
Schlägt bei Warnungen an |
|
Test::Exception |
Prüft, ob das Programm Exceptions erzeugt |
|
Test::Warn |
Prüft, ob Programm Warnungen richtig absetzt |
|
Test::Differences |
Grafische Darstellung von Unterschieden |
|
Test::LongString |
Prüft lange Strings |
|
Test::Output |
Fängt Ausgaben nach STDERR/STDOUT ab |
|
Test::Output::Tie |
Fängt Ausgaben an Filehandles ab |
|
Test::DatabaseRow |
Prüft Ergebnisse von Datenbankabfragen |
|
Test::MockModule |
Zusätzliche Module simulieren |
|
Test::MockObject |
Zusätzliche Objekte simulieren |
Das Testskript »simple.t« prüft mit »use_ok()« (aus Test::More) zunächst, ob sich das Modul überhaupt laden lässt. Der Konstruktor »new« gibt (hoffentlich) ein Objekt zurück. Die daraufhin aufgerufene Funktion »ok()« aus Test::More schreibt »ok 2« auf die Standardausgabe, falls das Objekt einen wahren Wert hat, und »not ok 2«, falls nicht. Ein optional an »ok()« übergebener dritter Parameter setzt einen Kommentar, damit klar wird, welcher Testfall gerade abläuft. Abbildung 1 zeigt die Ausgabe des Skripts.
Testfunktionen
Der dritte Testfall zeigt, wie nützlich die »is()«-Funktion aus Test::More bei Vergleichen ist. Geht etwas schief, zeigt das Testskript nicht nur den Kommentar und die jeweilige Zeile an, sondern auch den Unterschied zwischen dem erwarteten und dem tatsächlich erhaltenen Wert. Das Beispiel in Listing 1 provoziert als Demonstration einen Fehler (AbbilÂdung 1). Für sinnvolle Ausgaben, übergibt man den erhaltenen Wert als ersten, den erwarteten Wert als zweiten an »is()«.
|
Tabelle 2: |
|
|---|---|
|
Modul |
Funktion |
|
Test::Harness |
Standard-Harness |
|
Test::Builder |
Basis für neue Test-Utilities |
|
Test::Builder::Tester |
Test für neue Test-Utilities |
|
Test::Harness::Straps |
Basis für eine neuentwickelte Test-Harness |
|
Devel::Cover |
Analyse der Testabdeckung |
|
Test::Distribution |
Prüft Modul-Distributionen auf Vollständigkeit |
Die für den vierten TestÂfall eingesetzte FunkÂtion »like()« nimmt statt eines Vergleichswerts einen regulären AusÂdruck entgegen, auf den der erste Parameter passen muss, denn sonst ist eine Fehlermeldung das Ergebnis. Als abschließenden Kommentar gibt Test::More ein höfliches »Looks like you failed 1 tests of 4« aus.
Bei längeren Testskripten wäre es mühselig, ständig die Ausgabe zu beobachten und auf auftretende Fehler im vorbeisausenden Text zu achten. Zum Überwachen von Testsuiten, die gerne auch aus mehreren Dateien bestehen dürfen, bietet sich deswegen ein Hilfsmittel wie Test::Harness an, das alle Skripte ablaufen lässt und am Ende die Ergebnisse zusammenfasst.
Das Skript »prove«, das zum CPAN-Modul Test::Harness gehört, startet das Ganze. Die Perl 5.8 beiliegende Version von Test::Harness enthält das Skript nicht, es ist also wichtig, die neueste Version vom CPAN zu laden. Mit nur einem Testskript aufgerufen zeigt »prove« folgende Ausgabe:
$ prove ./simple.t ./simple....ok All tests successful. Files=1, Tests=4, 0 wallclock secs (0.08 cusr + 0.01 csys = 0.09 CPU)
Wer Teilergebnisse braucht, ruft »prove« mit der Option »-v« (für verbose) auf, dann erscheinen wieder die einzelnen »ok«- und »not ok«-Anzeigen mit den Testkommentaren. Falls die Testdatei einer Moduldistribution ausgeführt wird, ohne das Modul zu installieren, hilft der Parameter »-b«. Nach einem »make« ermöglicht er das Nutzen der im Verzeichnis »blib« liegenden Moduldateien.
Was »prove« von der Kommandozeile aus leistet, läuft bei CPAN-Modulen kurz vor der Installation mit »make test« ab. Der dafür verantwortliche »MakeMaker« schraubt dafür an Perls Bibliothekseinzugspfad »@INC« herum, um die Testsuite tatsächlich mit noch nicht installierten Modulen ablaufen zu lassen.
Tiefenwirkung
Neben Test::More gibt es viele Utility-Module auf dem CPAN, um Testcode zu erzeugen, ohne allzu oft dieselben Zeilen einzutippen. Ein Beispiel ist Test::Deep, das tief verschachÂtelte Strukturen vergleicht. Listing 2 zeigt einen kurzen Testfall, der die Funktion »get_mp3tag« des Moduls »MP3::Info« aufruft. Ist eine MP3-Datei ordentlich mit Tags versehen, liefert die Funktion eine Referenz auf einen Hash zurück, der eine Reihe von Schlüsseln wie etwa »ARTIST«, »ALBUM« und so weiter enthält.
Statt nun mühselig erst mal zu prüfen, ob es sich bei dem zurückgelieferten Ergebnis überhaupt um eine Hashreferenz handelt, und dann eine Reihe von erforderlichen Hashschlüsseln abzuklappern, schlägt die Funktion »cmp_deeply()« gleich alle Fliegen mit einer Klappe: Sie nimmt in den ersten zwei Argumenten Array- oder Hash-Referenzen entgegen, die sie vergleicht. So liefert »cmp_deeply($ref1, $ref2)« einen wahren Wert zurück, falls »$ref1« und »$ref2« auf gleiche Datenstrukturen zeigen.
|
Listing 1: |
|---|
01 #!/usr/bin/perl
02 use strict;
03 use warnings;
04
05 use Test::More tests => 4;
06
07 BEGIN { use_ok("Config::Patch"); }
08
09 my $p = Config::Patch->new(
10 key => "foo");
11
12 # True
13 ok($p, "New object");
14
15 # #1 eq #2
16 is($p->key, "Waaah!", "Retrieve key");
17
18 # #1 matches #2
19 like($p->key(), qr/^f/,
20 "Key starts with 'f'");
|
Aber das ist noch nicht alles: Der direkte Vergleich lässt sich mit einer Unzahl zusätzlicher Funktionen manipulieren, etwa um festzustellen, ob ein EleÂment der einen Datenstruktur nur über einen regulären Ausdruck mit dem des Gegenüber übereinstimmt. Die Funktion »re()« bewerkstelligt dies. Falls ein Element der Struktur eine Referenz auf einen Hash enthält, lässt sich mit »superhashof()« festlegen, dass der erste Hash nur eine Untermenge der Schlüssel des zweiten Hash enthalten darf.
Listing 3 prüft auf diese Weise gleich mehrere Dinge auf einmal: Ob »$tag« eine Hashreferenz ist und ob der referenzierte Hash unter anderem die Schlüssel »YEAR« und »ARTIST« enthält. Ob außerdem die im Hash unter den Schlüsseln gespeicherten Werte die jeweils angegebenen regulären Ausdrücke befriedigen, ob also im »ARTIST«-Tag nur Text mit Leerzeichen, in »YEAR« nur eine Zahl vorkommt.
Test::Deep bietet noch eine Reihe praktischer Markierungsfunktionen an, mit denen der Programmierer einfach Unterbäume der an »cmp_deeply« übergebenen Datenstrukturen prüft, ohne in »for«-Schleifen abzudriften. So legt »array_each()« fest, dass ein Knoten eine Referenz auf einen Array enthält, und führt einen als Parameter übergebenen Test (wie zum Beispiel »re()«) mit jedem Element des Array aus.
Neben dem gezeigten »superhashof()« gibt es »subhashof()«, für den Fall, dass der Referenzhash optionale Elemente enthält. Um festzustellen, ob ein Array eine Reihe von Elementen in beliebiger Reihenfolge, mit und ohne Wiederholung enthält, gibt es »set()« und »bag()«. Für optionale Elemente stehen, analog zu den Hash-Funktionen, »subbagof()«, »superbagof()«, »subsetof()« und »supersetof()« bereit.
|
Listing 2: |
|---|
01 #!/usr/bin/perl
02 use warnings;
03 use strict;
04
05 use Test::More tests => 1;
06 use Test::Deep;
07 use MP3::Info;
08
09 my $tag = get_mp3tag("Westerland.mp3");
10
11 cmp_deeply(
12 $tag,
13 superhashof({
14 YEAR => re(qr(^d+$)),
15 ARTIST => re(qr(^[sw]+$)),
16 }));
|
|
Listing 3: |
|---|
01 #!/usr/bin/perl -w
02 use strict;
03
04 package Foo;
05
06 sub new {
07 my($class) = @_;
08 bless {}, $class;
09 }
10
11 sub foo {
12 print "foo!n";
13 }
14
15 package main;
16
17 use Test::More tests => 1;
18
19 my $t = Foo->new();
20 isa_ok($t, "Foo", "New Foo object");
|
Abgefahrene Tests
Listing 3 zeigt die Definition einer Klasse »Foo« und ein anschließend ausgeführtes Testskript, das den Konstruktor der Klasse aufruft und mit »isa_ok()« prüft, ob tatsächlich ein Objekt der Klasse »Foo« zurückkommt. Doch die Testsuite hat eine Lücke: Sie führt niemals die Methode »foo()« der Klasse aus. Dort könnte sich noch ein gemeiner Laufzeitfehler verstecken – die Testsuite bekäme davon nichts mit.
Bei kleinen Projekten fällt so etwas dem Programmierer sofort auf, doch bei großen ist das Modul Devel::Cover vom CPAN hilfreich. Es prüft, wie viele mögliche Pfade die Suite tatsächlich abfährt. Der Aufruf des zu testenden Perlskripts mit
perl -MDevel::Cover coverme.t
erzeugt Abdeckungsdaten im Verzeichnis »cover_db«, die ein nachfolgender Aufruf von »cover« analysiert und grafisch aufbereitet.
Cover ist ein ausführbares Skript, das sich mit Devel::Cover installiert. Dirigiert man den Browser nach »cover_db/coverage.html«, erscheint (siehe Abbildung 2) eine übersichtliche Zusammenfassung der Abdeckungsdaten. Abbildung 3 zeigt die Abdeckung in der Testskriptdatei »coverme-t.t«, die unter »cover_db/coverme-t.html« zu finden ist.
Devel::Cover prüft nicht nur alle Funktionen und Methoden, sondern auch die Abdeckung aller »if«-, »else«- und sonstigen Zweige. Auch wenn es bei größeren Projekten faktisch unmöglich ist, alle Zweige abzudecken, ist es doch nützlich zu wissen, in welche Bereiche man noch etwas Arbeit investieren könnte, um die Abdeckung zu verbessern.
Test-Tools im Eigenbau
Natürlich erfüllen Analyse-Tools wie Test::Harness nur sehr allgemeine Anforderungen. Jedem Entwickler ist es freigestellt, das generische Werkzeug zu verwenden oder, bei spezielleren Anforderungen, eigene Analysetools für Testsuiten zu entwerfen. Damit die Basisfunktionalität wie das Parsen der TAP-Ausgaben nicht ständig neu erfunden wird, bietet Test::Harness::Straps eine Basisklasse, die sich beliebig für private Smoke-Tests erweitern lässt.
Wer Näheres über Tests in Perl wissen möchte, dem sei besonders das ganz hervorragende neue Buch [2] empfohlen, das viele weiterführende Erklärungen liefert. (jcb)
|
Infos |
|---|
|
[1] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2005/11/Perl] [2] Ian Langworth & Chromatic, Perl Testing: O\’Reilly 2005 |
|
Der Autor |
|---|
|
Michael Schilli arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat Goto Perl 5 (deutsch) und Perl Power (englisch) für Addison-Wesley geschrieben und ist unter [mschilli@perlmeister.com] zu erreichen. Seine Homepage: [http://perlmeister.com] |









