Vor einiger Zeit hatte ich mit Getty, einem anderen Padre-Entwickler, eine Diskussion. Er meinte, dass das schreiben von Tests eine Menge Zeit beansprucht und er es vorher noch nie gemacht hat. Doch er ließ es auf einen Versuch ankommen und war erfolgreich.
Perl test? Was ist das?
Viele Entwickler testen indem sie ihr Program starten oder es mit einem Webbrowser aufrufen und ein wenig darin rumklicken. Manchmal geraten sie sogar in die Versuchung es in ihrer gedachten Funktion zu nutzen
Perl tests sind wesentlich effizienter. Sie füttern das Programm mit vielen verschiedenen Daten und überprüfen dabei ob alles so ist wie es soll und ob Fehler korrekt abgefangen werden. Besser gesagt: Sie sollten es tun – wenn du sie schreibst.
The story begins…
Ich schrieb Padre::File::FTP kurz vor Fertigstellung der letzten Version. Das Ganze war in weniger als einer Stunde fertig und arbeitete problemlos, war jedoch in einigen Bereichen verbesserungswürdig. Eine dieser Verbesserungen, die exists – Methode, nahm ich mir heute morgen vor.
Dies ist die Methode:
sub exists {
my $self = shift;
return if !defined( $self->{_ftp} );
return $self->size ? 1 : 0;
}
Wenn ein Net::FTP – Objekt existiert, überprüfe die Größe der Datei. Eine Datei die Speicherplatz belegt muss auch existieren. Doch es gefiel mir nicht dass leere Dateien nicht erkannt wurden; dies sollte sich heute ändern.
FTP und Net::FTP besitzen keine “exist” Funktionen. Neben der Größenabfrage stellt FTP ein “ls” Kommando bereit, welches dem unter Linux und Windows (genannt “dir”) existierenden ähnlich ist. Doch ich hatte keine Ahnung ob mir das weiterhelfen würde. FTP-Server benutzen gerne ihr eigenes Format für Dateilisten und ich erwartete eine Menge Probleme.
Kenne den Feind
Net::FTP sagt über “ls”…
ls ( [ DIR ] )
Get a directory listing of DIR, or the current directory.In an array context, returns a list of lines returned from the server. In a scalar context, returns a reference to a list.
Kein Wort über das Format oder welche Daten zurückgegeben werden und auch kein Beispiel. Um ein solches zu bekommen, schrieb ich die folgende Zeile in die ->exists – Methode:
print STDERR join("\n",$self->{_ftp})."\n";
Aber… wie starte ich ->exists jetzt? Das war eine der Situationen in denen ein nettes kleines Test-Script viel Zeit sparen kann.
Ein neues Test-Script…
Padre bietet im Datei -> Neu – Menü einige Templates an, unter anderem auch für ein Test-Script, aber ich entschied mit einer Kopie des bestehenden t/92-padre-file.t anzufangen.
Die meisten Zeilen flogen einfach raus und ein paar Minuten später hatte ich folgendes Grundgerüst:
#!/usr/bin/perl
use strict;
use warnings;
use Test::More;
use Padre::File;
if ( ! $ENV{PADRE_NETWORK_T}) {
plan(tests => 1);
SKIP: {
skip 'This test file requires permission to connect to the internet.',1;}
exit;
}
my $file; # Define for later usage
done_testing();
Gerade ein paar Minuten alt, aber schon ein fertiges Test-Script – auch wenn es noch nichts testet.
Bisher besteht es nur aus den Perl-Grundlagen (Interpreter, Pragmas), Test::More (zum testen) und Padre::File (das zu testende Modul). Da es (für FTP-Tests) zwangsweise auf das Internet zugreifen muss, sollte es nicht ohne Zustimmung des Benutzers laufen. Wenn die dazu notwendige Umgebungsvariable nicht gesetzt ist, beendet es sich einfach mit einem entsprechenden Hinweis.
done_testing(); ist eine aus Test::More importierte Funktion um den erfolgreichen Abschluss der Tests zu vermelden. Fehlt dieser Aufruf, geht Test::More davon aus, daß das Test-Script abgebrochen wurde.
Die Tests
Zur Erinnerung, ich wollte die ->exists – Methode testen, also legen wir los:
# Plain file from CPAN
$file = Padre::File->new('ftp://ftp.cpan.org/pub/CPAN/README');
ok( defined($file), 'FTP: Create Padre::File object' );
ok( ref($file) eq 'Padre::File::FTP', 'FTP: Check module' );
ok( $file->{protocol} eq 'ftp', 'FTP: Check protocol' );
ok( $file->size > 0, 'FTP: file size' ); ok( $file->exists, 'FTP: Exists' );
Einer der vielen Vorteile von Test-Scripten lässt sich bereits in diesen paar Zeilen sehr gut sehen. Ich will die ->exists – Methode testen, also erstelle ich ein Padre::File – Objekt mittels einer FTP-URL. Drei einfache weitere Zeilen geben mir die Sicherheit einer definierten Testumgebung: Wurde ein Objekt erstellt? Ist es ein FTP-Objekt?
Alle weiteren Tests können jetzt auf einer klar definierten und eindeutigen Testumgebung aufbauen. Ohne diese Zeilen würde ich nur vermuten, das alles richtig gelaufen ist. Padre::File->new() hätte fehlschlagen oder mir ein HTTP – Objekt zurückliefern können. Jeder weitere Test wäre sinnlos und ich würde nach Fehlern suchen, die nicht existieren (da die FTP-Tests nicht zwangsweise auch bei HTTP funktionieren).
Ein simples “prove -l t/94-padre-file-remote.t” gab mir die Gewissheit, das die bestehende ->exists – Methode funktioniert und nebenbei noch die gewünschten Beispieldaten.
Die Ergebnisse ausnutzen
Hier ist die neue ->exists Methode:
sub exists {
my $self = shift;
return if !defined( $self->{_ftp} );
# Cache basename value
my $basename = $self->basename;
for ($self->{_ftp}->ls($self->{_file})) {
return 1 if $_ eq $self->{_file};
return 1 if $_ eq $basename;
}
# Fallback if ->ls didn't help. A file heaving a size should exist.
return 1 if $self->size;
return 0;
}
It loops through the ->ls – results looking for a full-path or filename-only match. If this fails returning true on match. It also tries the old ->size hack if ->ls failed (due to unexpected syntax or other bad things).
Sie geht alle von ->ls zurückgegebenen Ergebnisse zurück und sucht nach dem vollen Pfad oder dem reinen Dateinamen der gesuchten Datei. Sollte das nicht funktionieren, versucht sie es nochmal mit der alten Größenermittlung.
Noch mehr Tests…
Ein weiterer großer Vorteil von Test-Scripten: Sie können mit minimalem Aufwand wesentlich mehr Tests ausführen als dies manuell möglich wäre – und vor allem in wesentlich kürzerer Zeit.
Ein paar kopierte Zeilen (Padre hat hierfür die schöne Strg+D – Funktion), eine Schleife und folgendes Ergebnis entstand :
# Test some FTP servers
for my $url (
'ftp://ftp.ubuntu.com/ubuntu/project/ubuntu-archive-keyring.gpg',
'ftp://ftp.proftpd.org/README.MIRRORS', # Proftpd
'ftp://ftp.redhat.com/pub/redhat/linux/README', # vsftpd
'ftp://ftp.cisco.com/test.html', # Apache FTP
'ftp://ftp.gwdg.de/pub/mozilla.org/_please_use_ftp5.gwdg.de_', # Empty file
# TODO: Find a public FTP server using Microsoft FTP service and add it
) {
$url =~ /ftp\:\/\/(.+?)\// and my $server = $1;
$file = Padre::File->new($url);
ok( defined($file), 'FTP '.$server.': Create Padre::File object' );
ok( $file->exists, 'FTP '.$server.': Exists' );
my $size = $file->size;
ok( defined($size), 'FTP '.$server.': '.$size.' bytes' );
}
Diese paar Zeilen testen die ->exists und ->size – Methoden gegen 5 verschiedene URLs auf 5 verschiedenen FTP-Servern, jeder mit anderer Software. Übrigends: Die 5 URLs rauszusuchen dauerte länger als das Schreiben der Tests.
Ergebnisse
Die Entwicklung dieses Test-Scripts beanspruchte weniger Zeit als ich gebraucht hätte, um alle Tests auch nur einmal in Padre selbst auszuführen. Mit einem einfachen prove – Aufruf werden 26 Tests gegen Padre::File::FTP durchgeführt. Sobald sich jemand mit einem FTP-Server meldet, der Probleme macht, reicht eine zusätzliche Zeile (in der for-Schleife) um diesen zu testen.
Jeder Padre – Entwickler und Benutzer kann den Test starten und wir finden dadurch möglicherweise (zukünftige) Bugs noch bevor ihre Auswirkungen in Padre selbst entdeckt werden. Zu jedem Testfehler wird die Zeilennummer im Test-Script ausgegeben und erspart einem die Suche nach einem Bug, der seine Auswirkungen erst ein dutzend Funktionsaufrufe später zeigt.
Tests kosten Zeit, aber man spart durch sie meistens wesentlich mehr Zeit, spätestens wenn der erste Bug auftaucht, der mit einem (bestehenden) Testscript in Sekunden identifiziert oder zumindest eingegrenzt ist.
Das fertige Test-Script gibt es hier.
