Kwalitätskontrolle (Linux-Magazin, November 2005)

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.

Suite als Standard

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.

Listing 1: simple.t

    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.)

Tiefenwirkung

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.

Listing 2: mp3.t

    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 }));

Abgefahrene Tests

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.

Listing 3: coverme.t

    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

Lass mocken, Kumpel

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.

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2005/11/Perl

[2]
``Perl Testing'', Ian Langworth & chromatic, O'Reilly 2005.

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.