Test-Driven-Development verspricht Code mit weniger Fehlern und einer nebenbei generierten Testsuite. Das Tutorial nutzt ein neues CPAN-Modul.
Vor einigen Wochen hat mich die Firma auf einen Kurs zum Thema "Test Driven Development" (TDD) geschickt und um die neu erworbenen Kenntnisse in die Praxis umzusetzen, geht der Perl-Snapshot heute nach "agilen" Prinzipien vor. Schnelle und flexible Entwickler legen sofort los, ohne viel über Details nachzudenken. Da sie immer zuerst einen Test schreiben, bevor sie sich an die Implementierung einer Funktion machen, 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 möglich ist.
Die vor dem Schreiben einer Funktion geschriebenen Tests schlagen naturgemäß zunächst fehl, da das gewünschte Feature entweder noch gar nicht existiert oder nur teilweise oder fehlerhaft implementiert ist. Steht der Code schließlich, schaltet die Testsuite auf grün, was eine Entwicklungsumgebung wie Eclipse tatsächlich optisch so anzeigt.
1 package TestsFor::User; 2 use Test::Class::Moose; 3 4 sub test_constructor { 5 can_ok 'User', 'new'; 6 } 7 8 1;
1 #!/usr/local/bin/perl -w 2 use Test::Class::Moose::Load qw(t .); 3 Test::Class::Moose->new->runtests;
Um zum Beispiel eine Klasse User.pm für ein Login-System
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 instantiieren lässt. Listing
1 zeigt die Testdatei für einfache Testfälle, Basic.pm
,
die in Verzeichnis t
liegt und welches das Modul
brandneue Modul Test::Class::Moose vom CPAN 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, prüft also, ob die Klasse User
ihren Konstruktor
new
aufrufen kann. 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 anschließend alle test_*
-Routinen. Zu diesem
Zeitpunkt des Projekts existiert allerdings die Klasse User
noch nicht, und
so schlägt die Test-Suite wie in Abbildung 2 gezeigt in test_constructor
fehl.
Abbildung 1: Zunächst schlägt die Testsuite Alarm, denn die Klasse "User" existiert noch nicht. |
Der TDD-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
package User; use Moose; 1;
hinein. Graue Perlpanther reiben sich hier vielleicht ungläubig ihre müden Augen, denn
das Package User
definiert keinen Konstruktor new()
, der einen Objekthash $self
mittels bless()
mit einem Paket verschweißt. Das CPAN-Modul Moose erledigt
all dies hinter den Kulissen, so dass jedes Paket, das Moose hereinzieht, automatisch
einen Konstruktor new()
besitzt.
Ein erneuter Aufruf der Testsuite findet die neue .pm-Datei, die in ihr
enthaltene Klasse, und führt den Konstruktor new()
erfolgreich aus:
$ ./runtests ... ok 1 - TestsFor::User
Grünes Licht -- das Signal für den TDD-Entwickler, ein neues Feature anzugehen!
Das User-Objekt braucht Methoden, um die Email-Adresse des Users zu setzen und
abzufragen. Ein normal arbeitender Entwickler würde jetzt vielleicht gleich
anfangen, den scheinbar einfachen Code einzutippen. Nicht so der TDD-Anhänger, denn
der schreibt zunächst einen fehlschlagenden Test. Listing 3 definiert die Methode
test_accessors
, die das Test-Modul 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'
.
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;
Eine Zeile weiter holt der noch
nicht definierte Accessor den per Konstruktor gesetzten Email-String hervor und
die Funktion is
aus dem Modul Test::More vergleicht den Wert mit dem
vorher gesetzten. Stimmen beide überein, schreibt is
den String "ok"
in die
TAP-Ausgabe der Testsuite, die Testsuite erkennt dies als erfolgreich ausgeführten
Testfall. Es folgt ein Test des sogenannten Setters, der mittels der Methode
email()
einen neuen Wert für die Emailadresse des Users setzt und später
mit dem Accessor (ebenfalls email()
, diesmal ohne Argument) den gespeicherten
Wert wieder hervorholt und mit dem Original vergleicht.
Doch noch verfügt die Klasse User.pm noch nicht einmal über den notwendigen
Code und der neue Test schlägt sofort fehl.
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, mussten
Perl-Hacker der alten Garde vor dem Auftauchen von Moose noch dutzende von
Codezeilen händisch einfügen. Mit Moose ist dies 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 fortgeschrittenere
Version der Klasse User
, die mit has
das Attribut email
definiert.
Ihr Parameter is
gibt mit "rw"
an, dass der Wert sowohl geschrieben
also auch gelesen werden kann. Den Typ gibt isa
mit "Str" an, also einen
beliebigen Zeichenstring.
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 Entwicklung kann fortschreiten. |
Das erneute Anstoßen der Testsuite in Abbildung 2 zeigt, dass nun alle drei
definierten Testfälle erfolgreich durchlaufen. Es kann weitergehen! Als nächstes
schreibt das Pflichtenheft des Kunden vor, dass User unter ihrer Email in einer
Kundendatenbank registriert werden. Getreu den TDD-Prinzipien definiert
Listing 5 zuerst den Testfall mit der Routine test_customers()
. Es erzeugt
mittels des Konstruktors new()
der Klasse Customers
eine neue Kundendatei. Dann speist es zwei neue
User mit unterschiedlichen Email-Adressen mittels der noch nicht existierenden
Methode sign_up()
in die Datenbank ein. In der zweiten for-Schleife ab
Zeile 17 prüft die Testroutine dann mittels ok
und der zu implementierenden
Methode user_find_by_email
, ob das Kundendateiobjekt
die gerade registrierten Kunden auch wieder findet. In diesem Fall wird die
Methode per Vorschrift einen wahren Wert zurückliefern.
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;
Wieder stehen zunächst alle Räder still, denn die Testsuite zeigt einen Fehler an. Um ihn
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 denen die Methode sign_up()
das ihr übergebene
Objekt vom Typ User
unter dessen Email-Adress abliegt. Die Lookup-Methode
user_find_by_email()
sieht mittels exists
im globalen Hash nach und liefert
entweder das gefundene User-Objekt zurück, falls dieser vorher registriert wurde,
oder undef
falls sie ihn 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.
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;
Das CPAN-Modul Test::Class::Moose befindet sich noch in der Beta-Phase, und tatsächlich habe ich von seiner Existenz erst auf der YAPC-Konferenz Anfang Juni im texanischen Austin, Stunden vor Redaktionsschluss, erfahren. Es sieht nach meinem ersten Eindruck sehr stabil aus, aber der Autor des Moduls nimmt etwaige Bug-Reports oder Patches gerne 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% Codeabdeckung bietet. Kommt der Kunde dann urplötzlich mit Änderungswünschen mitten im Projekt daher, kann der TDD-Werker diese sorglos einbauen, denn die Testsuite garantiert, dass sich nicht nebenbei 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 erstellen. Der einfachste 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 so 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. Oder falls sich Teile des Systems als bekannte Software-Patterns herauskristallisieren, sollte der Entwickler sie in deren Referenzimplementierungen umwandeln. Dieses Refactoring ist natürlicher Bestandteil des Verfahrens und verursacht normalerweise keine Probleme, ebenfalls wegen der bereits bestehenden Testsuite und deren weitflächigen Code-Abdeckung. Zeigt die Testsuite grünes Licht, war der Frühjahrsputz erfolgreich.
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