Test-driven Development mit einer nebenbei generierten Testsuite verspricht Code mit weniger Fehlern. Michael “Perlmeister” Schilli betritt gleich den Pfad der Agilität und findet am Wegesrand zufällig ein passendes, nagelneues CPAN-Modul.
Vor einigen Wochen hatte mich mein Arbeitgeber auf einen Kurs zum Thema Test-driven Development (TDD) geschickt. Testgetriebenes Entwickeln zählt zu den so genannten agilen Methoden. Um mein gerade erworbenes Wissen in die Praxis umzusetzen, stelle ich den heutigen Perl-Snapshot ganz in den Dienst dieses Prinzips. Schnelle und flexible Entwickler legen sofort los, ohne auf Details zu achten. Sie schreiben stets zuerst einen Test, bevor sie sich an die Implementierung einer Funktion machen. Darum wächst die Testsuite automatisch mit relevanten Tests zu Funktionen des Systems mit. Unsauberen Code säubern sie später mittels Refactoring, was dank des durch die Testsuite bereitgestellten Sicherheitsnetzes gefahrlos gelingt.
Online PLUS
In einem Screencast demonstriert Michael Schilli das Beispiel: https://www.linux-magazin.de/plus/2013/08
Nichts als Fehler – und das ist richtig so
Die vor dem Schreiben einer Funktion entwickelten Tests schlagen naturgemäß fehl, da das gewünschte Feature zunächst entweder noch gar nicht existiert oder nur teilweise oder fehlerhaft implementiert ist. Steht später der Code, schaltet die Testsuite auf Grün, was eine Entwicklungsumgebung wie Eclipse sogar optisch so anzeigt.
Um zum Beispiel eine Klasse »User.pm« für ein Loginsystem zu schreiben, das später Methoden wie »login()« unterstützen soll, legt der TDD-Apostel zunächst einen Testfall an. Der prüft, ob sich die gewünschte Klasse überhaupt instanzieren lässt. Listing 1 zeigt die Testdatei für einfache Testfälle »Basic.pm« . Sie liegt im Verzeichnis »t« , welches das brandneue CPAN-Modul Test::Class::Moose nutzt. Letzteres führt alle Methoden, die mit dem Präfix »test_« beginnen, mit den darin enthaltenen Testroutinen aus.
Listing 1 definiert »test_constructor()« und setzt darin den Befehl
can_ok 'User', 'new';
aus dem Modul Test::More ab. Damit prüft »test_constructor()« , ob die Klasse »User« dazu fähig ist, ihren Konstruktor »new« aufzurufen.
Listing 2 zeigt ein Skript, das die Testsuite ablaufen lässt. Zunächst lädt es mittels »Load« alle Perl-Module mit der Endung ».pm« , die es in den angegebenen Unterverzeichnissen ».« und »t« findet. Die Methode »runtests()« durchstöbert daraufhin alle »test_*« -Routinen. In dieser Projektphase existiert die Klasse »User« allerdings noch nicht, und so schlägt die Testsuite in »test_constructor()« fehl (Abbildung 1).
Listing 1
Basic.pm
1 package TestsFor::User;
2 use Test::Class::Moose;
3
4 sub test_constructor {
5 can_ok 'User', 'new';
6 }
7
8 1;
Listing 2
runtests
1 #!/usr/local/bin/perl -w 2 use Test::Class::Moose::Load qw(t .); 3 Test::Class::Moose->new->runtests;

Abbildung 1: Zunächst schlägt die Testsuite Alarm, denn die Klasse »User« hat noch niemand geschrieben.
Erfolgserlebnisse
Der Test-driven-Entwickler hat dies zweifellos erwartet und setzt nun alles daran, Code hinzuzufügen, bis die Testsuite erfolgreich durchläuft. Da die Klasse nicht existiert, legt er eine neue Datei »User.pm« an und schreibt hinein:
package User; use Moose; 1;
In Würde ergraute Perlpanther reiben sich hier vielleicht ungläubig die Augen, denn das Package »User« definiert keinen Konstruktor »new()« , der einen Objekthash »$self« mittels »bless()« mit einem Paket verschweißt. Das CPAN-Modul Moose [2] erledigt all dies hinter den Kulissen, sodass jedes Paket, das Moose hereinzieht, automatisch einen Konstruktor »new()« besitzt. Ein erneuter Aufruf der Testsuite mit »./runtests« liefert:
[...] ok 1 - TestsFor::User
Die Suite findet also die neue ».pm« -Datei, die in ihr enthaltene Klasse, und führt den Konstruktor »new()« erfolgreich aus.
Erneut geht’s hinein ins agile Getümmel
Grünes Licht – das Signal für den TDD-Entwickler, ein neues Feature anzugehen! Das User-Objekt braucht Methoden, um die E-Mail-Adresse des Users zu setzen und abzufragen. Ein konventionell arbeitender Entwickler würde wohl sogleich anfangen den scheinbar einfachen Code einzutippen. Nicht so der TDD-Anhänger, denn der schreibt zunächst einen Test, der noch dazu fehlschlägt.
Listing 3 definiert die Methode »test_accessors()« , die das Testmodul später ebenfalls aufgrund des Präfixes finden und aufrufen wird. Sie erzeugt ein neues Objekt vom Typ »User« und übergibt dem Konstruktor das Parameterpaar »email => ‘a@b.com’« . Eine Zeile weiter holt der noch nicht definierte Accessor den per Konstruktor gesetzten E-Mail-String hervor, und die Funktion »is« aus dem Modul Test::More vergleicht den Wert mit dem vorher gesetzten. Stimmen beide Inhalte überein, schreibt »is« den String »ok« in die TAP-Ausgabe der Testsuite. Die Suite erkennt dies als erfolgreich ausgeführten Testfall.
Es folgt ein Test des so genannten Setters, der mit der Methode »email()« einen neuen Wert für die E-Mail-Adresse des Users setzt und später mit dem Accessor (ebenfalls »email()« , aber ohne Argument) den gespeicherten Wert wieder hervorholt und mit dem Original vergleicht. Doch noch verfügt die Klasse »User.pm« nicht einmal über den notwendigen Code – der neue Test schlägt also sofort fehl.
Listing 3
Accessors.pm
01 package TestsFor::User;
02 use Test::Class::Moose;
03
04 sub test_accessors {
05
06 my $email1 = 'a@b.com';
07 my $email2 = 'c@d.com';
08
09 my $user = User->new(
10 email => $email1,
11 );
12 is $user->email(), $email1;
13
14 # Setter
15 $user->email( $email2 );
16 is $user->email(), $email2;
17 }
18
19 1;
Getter und Setter
Damit der Konstruktor der Klasse »User« den Email-String als benamten Parameter entgegennimmt, eine gleichnamige Accessor-Methode ihn wieder ausspuckt und ein Setter neue Werte setzen kann, musste in der Zeit vor Moose der Perl-Hacker Dutzende Codezeilen händisch einfügen. Mit Moose ist dies ein Klacks, denn dessen Funktion »has« definiert ein Attribut einer Klasse, das sich gleichzeitig über einen Konstruktor-Parameter, einen Getter (»email()« ) und einen Setter (»email( $email )« ) ansprechen lässt.
Listing 4 zeigt eine spätere Version der Klasse »User« , die per »has« das Attribut »email« definiert. Ihr Parameter »is« macht mit »rw« den Wert les- und beschreibbar, »isa« bestimmt ihn als »Str« , also als beliebige Zeichenkette. Das erneute Anstoßen der Testsuite in Abbildung 2 zeigt, dass nun alle drei definierten Testfälle erfolgreich durchlaufen. Es kann also weitergehen!
Listing 4
User.pm
1 package User; 2 use Moose; 3 4 has 'email' => 5 (is => 'rw', isa => 'Str'); 6 7 1;

Abbildung 2: Grünes Licht: Die Testsuite läuft ohne Fehler durch, die agile Entwicklung darf darum weiter voranschreiten.
Die Kunden sollen in eine Datenbank
Als Nächstes schreibt das Pflichtenheft des Kunden vor, dass User sich unter ihrer E-Mail in einer Kundendatenbank registrieren. Getreu den TDD-Prinzipien definiert Listing 5 zuerst den Testfall mit der Routine »test_customers()« . Es erzeugt mit Hilfe des Konstruktors »new()« der Klasse »Customers« eine neue Kundendatei.
Dann speist es zwei neue User mit ihren jeweiligen E-Mail-Adressen mit der noch nicht existierenden Methode »sign_up()« in die Datenbank ein. In der zweiten For-Schleife ab Zeile 17 prüft die Testroutine nun mit »ok« und der zu implementierenden Methode »user_find_by_email()« , ob das Kundendatei-Objekt die gerade registrierten Kunden auch wiederfindet. In diesem Fall wird die Methode per Vorschrift einen wahren Wert zurückliefern.
Listing 5
Register.pm
01 package TestsFor::Customers;
02 use Test::Class::Moose;
03
04 sub test_customers {
05
06 my $customers = Customers->new();
07
08 my @users = qw( a@b.com c@d.com );
09
10 for my $email ( @users ) {
11 my $user = User->new(
12 email => $email,
13 );
14 $customers->sign_up( $user );
15 }
16
17 for my $email ( @users ) {
18 ok $customers->user_find_by_email(
19 $email
20 );
21 }
22 }
23
24 1;
Als Letztes: Die Fahndung nach den Usern
Wieder stehen alle Räder still, wenn die fehlerhafte Testsuite es so will. Um den Fehler zu beheben, implementiert Listing 6 die Klasse »Customers« , ebenfalls wieder mit Moose und zwei zusätzlichen Methoden. Perls Objektsystem übergibt ihnen wie üblich als erstes Argument eine Referenz auf das Objekt. Die Klasse definiert einen globalen Hash »%USERS« , in dem die Methode »sign_up()« das ihr übergebene Objekt vom Typ »User« unter dessen E-Mail-Adresse ablegt.
Die Lookup-Methode »user_find_by_email()« sieht mit »exists« im globalen Hash nach und liefert entweder das gefundene User-Objekt zurück, falls es schon registriert ist, oder »undef« , falls sie den User nicht findet. Sobald der Code in Listing 6 fehlerfrei ist, leuchtet grünes Licht auf und ein weiterer Meilenstein im Projekt ist unter Dach und Fach.
Listing 6
Customers.pm
01 package Customers;
02 use Moose;
03
04 our %USERS = ();
05
06 sub sign_up {
07 my( $self, $user ) = @_;
08
09 $USERS{ $user->email() } = $user;
10 }
11
12 sub user_find_by_email {
13 my( $self, $email ) = @_;
14
15 return exists $USERS{ $email };
16 }
17
18 1;
Moose – gefunden, ohne danach gesucht zu haben
Das CPAN-Modul Test::Class::Moose befindet sich noch in der Betaphase, und tatsächlich habe ich von seiner Existenz erst auf der Perl-Konferenz YAPC [3] Anfang Juni im texanischen Austin erfahren – wenige Stunden vor Redaktionsschluss. Es sieht nach meinem ersten Eindruck sehr stabil aus, gleichwohl nimmt der Autor des Moduls Bugreports oder Patches freudig entgegen.
Der Vorteil der Entwicklung nach der Test-driven-Development-Methode ist zweifellos die stets wachsende Testsuite, die – falls der Entwickler nach Plan vorgeht – praktisch 100 Prozent Code-Abdeckung bietet. Kommt der Kunde mitten im Projekt plötzlich mit Änderungswünschen, kann der TDD-Werker diese sorglos einbauen, denn die Testsuite garantiert, dass sich dabei keine fatalen Fehler einschleichen.
Agile Entwickler sollten sich auch nicht zu sehr den Kopf darüber zerbrechen, was nun die eleganteste Methode ist, ein bestimmtes Feature zu implementieren. Ein einfacher Weg genügt, und sobald die Testsuite Grün meldet, geht es weiter zum nächsten Feature.
Nach einiger Zeit der Entwicklung im Schnell-schnell-Verfahren entstehen naturgemäß hässliche Codestücke, die alle paar Iterationen korrigiert gehören, damit die Software wartbar bleibt: Findet sich ein dupliziertes Codestück, lässt es sich meist in eine Funktion auslagern. Kristallisieren sich Teile des Systems als bekannte Softwarepatterns heraus, sollte der Entwickler sie in deren Referenzimplementierungen umwandeln.
Dieses so genannte Refactoring ist natürlicher Bestandteil des Verfahrens und verursacht normalerweise keine Probleme – ebenfalls dank der seit Beginn bestehenden Testsuite und ihrer weitflächigen Code-Abdeckung. Zeigt die Testsuite grünes Licht, war der Frühjahrsputz erfolgreich.
Infos
- Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2013/08/Perl
- Test::Class::Moose: http://search.cpan.org/~ovid/Test-Class-Moose-0.12/lib/Test/Class/Moose.pm
- Yet Another Perl Conference: http://www.yapc.org







