Regressionstests sind selbst für kleine Softwareprojekte unerlässlich. Eine langsam aufgebaute Suite erlaubt es, während der Entwicklung und auch in der später folgenden Wartungsphase Fehler zu korrigieren und Teile des Systems umzuschreiben, ohne die Sorge, die bestehende Codebasis dabei zu ruinieren.
Dass ein Programm auf Anhieb funktioniert, ist so unwahrscheinlich, dass es, falls es doch einmal klappt, verdächtig erscheint und eher auf tieferliegende Probleme hindeutet. Testgetriebene Entwicklung hat sich deshalb im Zuge der Extreme-Programming-Strategie durchgesetzt. Ein schnell in die Suite eingehängter Testfall geht zunächst schief, funktioniert dann aber plötzlich mit einem neu implementierten Feature. Das motiviert nicht nur während der Entwicklung (kleine Erfolgserlebnisse: ``Yay!''), sondern läuft später als fester Bestandteil der Suite immer und immer wieder ab. So summieren sich unmerklich kleine Schritte zu einem Gesamtwerk auf, das später keine QA-Abteilung der Welt zustande brächte.
Auch bei der Weiterentwicklung und späterem Refactoring stellt sich die Frage: Wer garantiert, dass ein gefixter Bug keine unerwünschten Nebenwirkungen hat? Wer ohne Aufwand ein paar hundert Tests ablaufen lassen kann, tauscht ohne Herzklopfen Teile des Systems aus und schläft ruhiger, während ein neuer Release von den ungewaschenen Massen im Internet auf die Probe gestellt wird. Selbst wer Bewegungen wie Extreme Programming für neumodischen Firlefanz hält, kommt an einer regressionsfähigen Testsuite nicht vorbei.
In der Perl-Community gehört es zum Glück zum guten Ton, einem CPAN-Modul eine Testsuite beizulegen, die die wichtigsten Funktionen überprüft.
Was tun, wenn kein Modul, sondern nur ein 'schnelles' Skript entsteht? Über die Jahre habe ich festgestellt, dass ein Skript nur Kommandozeilenparameter verarbeiten, die Dokumentation ausspucken und für alles weitere Module verwenden sollte. Alles, was darüber hinausgeht, sollte so in ein selbstgeschriebenes neues Modul verpackt werden, von dem dann eventuell auch andere Skripts profitieren können. Und selbstverständlich liegt jedem Modul Dokumentation und eine Testsuite bei, oder?
Das TAP-(Test-Anything-Protocol) hat sich in
der Perl-Welt für Regressionstests
durchgesetzt. Nach einer Kopfzeile, die
die Anzahl der folgenden Tests festlegt,
geben die Testfälle im Erfolgsfall ok
und
im Fehlerfall not ok
aus:
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überliegende Test-Harness um eine Zusammenfassung. In wenigen Zeilen gibt sie aus, ob alles glattging oder wieviele Testfälle schief gingen.
Listing simple.t
zeigt ein Beispiel. Traditionell
haben Perl-Testskripts die Endung *.t
und liegen
im Verzeichnis t
einer Modul-Distribution.
Da bei Testfällen oft ähnliche Sachen geprüft
und Aktionen eingeleitet werden, gibt es spezielle
Test-Module wie Test::More
, die die Arbeit erleichtern
und Code-Duplizierung vermeiden helfen.
simple.t
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 werden. Das ist
wichtig, denn falls die Testsuite vorher unerwartet abbricht,
sollte dies angezeigt werden. Während eifriger Erweiterungsphasen
der Testsuite weichen manche Entwickler diese Prüfung mit
use Test::More qw(no_plan);
temporär auf, aber letztendlich ist eine fest vorgegebene Testzahl der empfohlene Weg.
Das Testskript simple.t
prüft als erstes
mit use_ok()
(aus Test::More
exportiert), ob sich das Modul überhaupt laden lässt.
Der Konstruktor new
gibt (hoffentlich) ein Objekt
zurück. Die darauf aufgerufene Funktion ok()
aus
Test::More
schreibt ok 2
auf die Standardausgabe,
falls das Objekt einen wahren Wert führt, und
not ok 2
, falls nicht. Ein optional an ok()
übergebener
dritter Parameter setzt einen Kommentar, damit in
der Ausgabe klar wird, welcher Testfall gerade abläuft.
Abbildung 1 zeigt die Ausgabe des Skripts.
Der dritte Testfall zeigt, wie nützlich es ist,
statt ok()
die is()
-Funktion aus Test::More
zu verwenden, falls
es etwas zu vergleichen gibt. Geht etwas schief (wie
der absichtlich herbeigeführte Fehler offenbart), zeigt das
Testskript nicht nur den Testkommentar und die entsprechende
Zeile im Testskript an, sondern auch den Unterschied zwischen
dem erwarteten und tatsächlich erhaltenen Wert an.
Damit die Ausgabe sinnvoll ist, muss der erhaltene Wert
als erster, der erwartete Wert als zweiter an is()
übergeben werden.
Die im vierten Testfall verwendete Funktion like()
nimmt
statt eines Vergleichswerts einen regulären Ausdruck entgegen,
auf den der erste Parameter passen muss. Ist dies nicht der Fall,
erfolgt, ähnlich wie bei is()
, eine detaillierte Fehlermeldung.
Als abschließenden Kommentar gibt
prove
ein höfliches ``Looks like you failed 1 tests of 4'' aus.
Wichtig: Immer höflich bleiben, niemand möchte einen verhärmten
QA-Bürokraten meckern hören.
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'");
Abbildung 1: Ausgabe des Testskripts |
Bei längeren Testskripts wäre es mühselig, ständig die Ausgabe zu beobachten und auf eventuell auftretende Fehler im vorbeisausenden Text zu achten. Zum Überwachen von Testsuiten, die gerne auch aus mehreren Dateien bestehen dürfen, bietet sich deswegen die Verwendung einer Test-Harness an. Sie lässt alle Skripts ablaufen, fasst die Ergebnisse zusammen und zeigt sie am Ende komprimiert an.
Die Test-Harness läuft mit dem Skript prove
ab, das
automatisch mit dem CPAN-Modul Test::Harness
installiert
wird. Die perl-5.8 beiliegende
Version von Test::Harness
enthält das Skript noch
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)
Falls doch Teilergebnisse interessieren, wird prove
mit der
Option -v
(für verbose) aufgerufen, dann erscheinen die
einzelnen ok
und not ok
Anzeigen samt den Testkommentaren
wieder. Falls eine
Testdatei einer Moduldistribution ausgeführt wird, ohne
das Modul zu installieren, hilft der Parameter -b
, die
nach einem make
im Verzeichnis blib
liegenden Moduldateien zu nutzen.
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.
Kasten:
Modul Funktion
Test-Utilities
Test::Simple Gebräuchlichste Test-Utility (TU), 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 Exceptions geworfen werden Test::Warn Prüft, ob Warnungen richtig abgesetzt werden Test::Differences Grafische Darstellung von Unterschieden in Strings und Strukturen 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
Analyse-Tools
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 (POD-Coverage, $VERSION, PREREQS, usw.)
Neben Test::More
gibt es eine Unzahl von Utility-Modulen auf dem
CPAN, die es erleichtern, Testcode zu erzeugen, ohne allzu oft
dieselben Maßnahmen einzutippen. Ein Beispiel ist Test::Deep
,
das tief verschachtelte Strukturen vergleicht.
Listing mp3.t
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
ARTIST
, ALBUM
und so weiter enthält. Statt nun mühselig
erst zu prüfen, ob es sich bei dem zurückgelieferten Ergebnis
überhaupt um eine Hashreferenz handelt und dann eine Reihe
von erforderlichen Hash-Schlüsseln abzuklappern, schlägt
die Funktion cmp_deeply
gleich alle Fliegen mit einer Klappe.
cmp_deeply
nimmt in den ersten zwei Argumenten Array- oder
Hash-Referenzen entgegen, denen es bis in die Tiefe folgt und bis ins
Detail vergleicht. cmp_deeply($ref1, $ref2)
liefert also einen
wahren Wert zurück, falls $ref1
und $ref2
auf gleiche
(wenn auch nicht notwendigerweise dieselben) Datenstrukturen zeigen.
Aber das ist noch nicht alles: Der direkte Vergleich lässt sich mit
einer Unzahl von zusätzlichen Funktionen manipulieren. So lässt sich
feststellen, ob ein Element der einen Datenstruktur nur über einen
regulären Ausdruck mit dem des Gegenübers übereinstimmt. Die
Funktion re()
bewerkstelligt dies. Oder 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 Hashes beherbergen muss. Listing
mp3.t
prüft so gleich mehrere Dinge auf einmal: Dass $tag
eine Hashreferenz ist und dass der referenzierte Hash unter anderem
die Schlüssel YEAR
und ARTIST
enthält, und dass die im
Hash unter den Schlüsseln gespeicherten Werte die jeweils angegebenen
regulären Ausdrücke befriedigen: Text mit Leerzeichen im ARTIST
-Tag
und eine Zahl in YEAR
. Test::Deep
bietet noch eine Reihe von
praktischen Markierungsfunktionen, mit denen man einfach Unterbäume
der an cmp_deeply
übergebenen Datenstrukturen prüft, ohne in
for
-Schleifen abzudriften. array_each()
legt fest, dass ein
Knoten eine Referenz auf einen
Array enthält und führt einen als Parameter übergebenen
Test (wie z.B. re()
) auf jedes Element des Arrays aus. Neben
dem gezeigten superhashof()
gibt es subhashof()
, wenn der
Referenzhash optionale Elemente enthält. Und auch um festzustellen,
ob ein Array einen Reihe von Elementen in beliebiger Reihenfolge, mit
und ohne Wiederholung enthält, gibt es bag()
und set()
. Für
optionale Elemente stehen, analog zu den Hash-Funktionen,
subbagof()
, superbagof()
, subsetof()
und supersetof()
bereit.
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(^[\s\w]+$)), 16 }));
Listing coverme.t
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 ein gemeiner Laufzeitfehler
verstecken, und die Testsuite bekäme davon nichts mit.
Bei kleinen Projekten fällt dergleichen dem Entwickler sofort auf,
doch bei großen ist das Modul Devel::Cover
vom CPAN
hilfreich, das prüft, wieviele 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
, das ein nachfolgender
Aufruf von cover
(ein ausführbares Skript, das mit
Devel::Cover
installiert wird) analysiert und graphisch aufbereitet.
Dirigiert man den Browser nach cover_db/coverage.html
, lässt sich,
wie in Abbildung 2 gezeigt, eine schöne Zusammenfassung der Abdeckungsdaten
ansehen. Abbildung 3 zeigt die Abdeckung in der Testskriptdatei
coverme-t.t
, die unter cover_db/coverme-t.html
erhältlich 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.
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");
Abbildung 2: Testabdeckung: Nicht alle Methoden wurden aufgerufen |
Abbildung 3: Abgedeckte Funktionen/Methoden |
Eine wesentliche Anforderung an eine Testsuite ist, dass sie schnell
ausführbar ist, ohne dass ein Entwickler viel
installieren oder konfigurieren muss. Aber viele Anwendungen koppeln
an komplizierte Datenbanken an oder brauchen eine funktionsfähige
Internetverbindung und einen bestimmten Server. Um solche Anforderungen
zu umgehen, lassen sich mit den Mock-Utilities Test::MockModule
und Test::MockObject
Pappkameraden erstellen, die zwar während
einer Testsuite täuschend echt Internet-Server oder Datenbanken
simulieren, aber letztendlich nur Attrappen sind.
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 Analyse-Tools
für Testsuiten zu entwerfen. Damit die Basisfunktionalität wie das
Parsen der TAP-Ausgaben nicht ständig neu erfunden werden muss, bietet
Test::Harness::Straps
eine Basisklasse, die sich beliebig für
private Smoke-Tests erweitern lässt.
Wer mehr über Tests in Perl wissen möchte, dem sei das ganz hervorragende neue Buch [2] empfohlen, das ausführlichere Erklärungen zu allen hier vorgestellten Modulen zeigt und noch mehr Test-Tipps gibt.
Michael Schilliarbeitet 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. |