Tests make things better

... und Perl ist mittendrin!

Wer kennt sie nicht - die berühmt berüchtigten Bluescreens oder andere Fehlermeldungen? So ziemlich jeder dürfte schonmal vor dem Computer gehockt und eben diesen verflucht haben. Warum passiert so etwas? Und warum ausgerechnet dann wenn ich es überhaupt nicht gebrauchen kann? Wir sollten unseren Kunden und uns selbst so etwas nicht zumuten. Ein einfaches - aber auch ungeliebtes - Mittel sind Tests. Die Tests können zwar keine 100%ige Sicherheit auf Fehlerfreiheit geben, aber sie erhöhen die Wahrscheinlichkeit, dass das Programm richtig funktioniert.

Bei den Modulen zum Testen von Modulen und der Dokumentation wird auf jede einzelne Methode eingegangen, da diese Methoden in Testskripten sehr häufig verwendet werden. Bei den Modulen zum Testen von Web-Applikationen und Konsolen-Programmen wird nur eine allgemeine Erläuterung gegeben.

In diesem Artikel werden einige Module aus dem Test::*-Namespace vorgestellt und es wird zum Schluss an Hand einer beispielhaften Modulentwicklung gezeigt, wie man von Anfang an mit Tests umgeht. Bevor es aber losgeht, werden noch ein paar allgemeine Dinge zu Tests genannt.

Warum Tests?

Tests haben einen ``schlechten Ruf'', weil es kein produktiver Code ist. Was aber häufig nicht gesehen wird, ist die Tatsache, das Tests den produktiv-Code noch produktiver macht. Kaum ein Entwickler schreibt gerne Tests, aber wenn man klein anfängt, ist das alles gar nicht mehr so schlimm.

``Ich brauche keine Tests!''

... waren vielleicht die letzten Worte des Ingenieurs, der die Software für die Ariane 5-Rakete einsetzte. 1996 stürzte eine unbenannte Ariane-5 Rakete ab, weil veraltete Software eingesetzt wurde, die noch aus Ariane 4-Zeiten stammte und nicht an die Ariane 5-Begebenheiten angepasst wurden.

Viele ``herrliche'' Bugs und ihre Auswirkungen sind unter http://www5.informatik.tu-muenchen.de/~huckle/bugs.html zu finden. Diese Beispiele zeigen, dass Tests nicht nur ``Schönheitsfehler'' finden, sondern auch erhebliche Kosten einsparen können.

Programmierer sind Menschen und Menschen machen Fehler. Das ist ja auch (meistens) nicht schlimm, aber man sollte doch versuchen, die Auswirkungen zu begrenzen. Diese Fehler sollten auch nicht unbedingt zu Kunden gelangen.

``Ich weiß was mein Skript macht''

Das will auch niemand anzweifeln. Doch manchmal kommt der Chef am Freitag nachmittag rein und möchte noch etwas am Programm geändert haben - bis zum Wochenende. Da kommt Hektik und Unruhe auf. ``Da muss ich nur an dieser Stelle etwas ändern ...''. Sicher, dass es nur diese eine Stelle war? In der Hektik oder wenn man sein Skript mal zwei, drei Monate aus den Augen gelassen hat, übersieht man doch mal was.

Wenn man nach einer Änderung die Tests laufen lässt, kann man gleich sehen, ob man nicht doch aus Versehen einen neuen Bug eingebaut hat.

Test-driven Development

Es gibt in der Softwareentwicklung auch den Ansatz, dass die Tests vor dem eigentlichen Code existieren. Wie später gezeigt wird (im Abschnitt Test::More), bietet auch Perl eine geeignete Möglichkeit, dies umzusetzen.

Philosophie der Testautomatisierung

Bei der Testautomatisierung gibt es ein paar Dinge, die man beachten sollte. Allerdings ist das Schreiben von Tests etwas ``lockerer'' als das Schreiben von Produktivcode.

Generell gilt, dass ein Test besser ist als gar keiner. Man kann also mit einem kleinen Satz an Testskripten starten und dann immer weiter aufbauen. Es sollte für jeden gefundenen Bug ein neuer Test geschrieben werden, der auch überprüft, dass dieser Bug tatsächlich behoben wurde. Damit stellt man auch gleichzeitig sicher, dass dieser Bug nicht mehr im Produktivcode auftaucht. Wenn es tatsächlich ein Bug war, sollte der neue Code des Moduls die neuen Tests bestehen, aber die alten Tests nicht. Wenn der neue Code des Moduls die alten Tests besteht, hat man den Bug nicht behoben.

In den folgenden Abschnitten werden noch ein paar Dinge genannt, die man bei der Erstellung einer Testsuite beachten sollte. Dies ist kein Muss, aber es hilft, die Tests auch richtig zu schreiben.

Testfälle generieren

Zu diesem Punkt sollte man gleich sagen, dass dies nicht immer möglich ist. In einigen Fällen muss man auf bestimmte Testfälle zurückgreifen und kann keine Testfälle automatisch generieren. Doch wo es geht, sollten die Testfälle generiert werden, um ein größeres Spektrum zu testen.

Als Beispiel soll hier mal folgendes Modul dienen:

   1 package TestPackage;
   2
   3 use strict;
   4 use warnings;
   5 
   6 sub summe{
   7     return 12;
   8 }
   9
     10 1;

In diesem einfachen Beispiel sieht man sofort, dass in der Funktion summe nicht wirklich eine Summe gebildet wird. Aber nicht alle Module sind so übersichtlich und Fehler fallen sofort auf. Manchmal sind es hunderte Zeilen von Code, die einen Fehler verursachen können.

Nun aber zurück zu dem Thema, warum Testfälle generiert werden sollen. Wenn nur hartcodierte Testfälle berücksichtigt werden, können Tests erfolgreich sein, obwohl das Modul eigentlich fehlerhaft ist.

Das folgende Testskript läuft ohne Fehler durch, obwohl das oben gezeigte Modul nicht korrekt arbeitet:

   1 #!/usr/bin/perl
   2
   3 use strict;
   4 use warnings;
   5 use TestPackage;
   6 use Test::More tests => 4;
   7
   8 my @testvalues = ([1,11],[2,10]);
   9 for my $ref (@testvalues){
     10     is(TestPackage::summe(@$ref),12);
     11 }
     12
     13 my @test2      = ([6,6],[5,7]);
     14 for my $arref (@test2){
     15     my ($sum1,$sum2) = @$arref;
     16     is($sum1 + $sum2 - TestPackage::summe(@$arref),0);
     17 }

Die Ausgabe ist absolut super! 0 Fehler! Das Modul kann ja nur korrekt arbeiten.

  C:\programs>perl testskript.pl
  1..4
  ok 1
  ok 2
  ok 3
  ok 4

Wenn man hier Testfälle generiert, dann fällt schnell auf, dass das Modul vielleicht doch nicht so korrekt arbeitet.

Das Testskript angepasst sieht dann so aus:

   1 #!/usr/bin/perl
   2
   3 use strict;
   4 use warnings;
   5 use TestPackage;
   6 use Test::More tests => 5;
   7 
   8 for my $counter (0..4){
   9     my ($sum1,$sum2) = (int rand 100, int rand 40);
     10     is(TestPackage::summe($sum1,$sum2),$sum1 + $sum2);
     11 }

und schon sieht das Testergebnis gar nicht mehr so toll aus (Werte können variieren):

  C:\programs>perl testskript.pl
  1..5
  not ok 1
  #   Failed test in testskript.pl at line 10.
  #          got: '12'
  #     expected: '55'
  not ok 2
  #   Failed test in testskript.pl at line 10.
  #          got: '12'
  #     expected: '100'
  not ok 3
  #   Failed test in testskript.pl at line 10.
  #          got: '12'
  #     expected: '22'
  not ok 4
  #   Failed test in testskript.pl at line 10.
  #          got: '12'
  #     expected: '98'
  not ok 5
  #   Failed test in testskript.pl at line 10.
  #          got: '12'
  #     expected: '62'
  # Looks like you failed 5 tests of 5.

Wenn das Modul dann korrigiert ist

   1 package TestPackage;
   2 
   3 use strict;
   4 use warnings;
   5
   6 sub summe{
   7     my ($sum1,$sum2) = @_;
   8     return $sum1 + $sum2;
   9 }
     10
     11 1;

dann läuft auch der Test durch:

  C:\programs>perl testskript.pl
  1..5
  ok 1
  ok 2
  ok 3
  ok 4
  ok 5

False Dilemma

Unter dem ``False Dilemma'' versteht man den Fall, dass fehlerhafter Code mit einem falschen Test zu einem scheinbaren Erfolg führt.

Wenn ein Skript zum Beispiel das Quadrat einer Zahl berechnen soll und der Code so aussieht

   1 package Quadrat;
   2
   3 sub quadrat{
   4     my ($zahl) = @_;
   5     return $zahl + $zahl;
   6 }

Wenn ich einen Test schreibe, der so aussieht:

   1 #!/usr/bin/perl
   2
   3 use strict;
   4 use warnings;
   5 use Test::More tests => 2;
   6
   7 use_ok("Test::More");
   8 is(Quadrat::quadrat(2),4);

dann läuft der Test durch. Ist ja auch scheinbar richtig, da 4 das Quadrat von 2 ist.

Aus diesem Grund sollte man viele Tests machen.

Testreihenfolge

Bei Tests sollte man auch einen Blick auf die Reihenfolge der Tests werfen. Tests sollten immer wieder in anderen Reihenfolgen durchgeführt werden. Auch Tests, die eigentlich andere Tests vorher benötigen, die Variablen setzen, sollten durchgemischt werden. Dann so kann man überprüfen, ob das Programm auch wirklich die fehlenden Variablen anmeckert. Durch eine feste Testreihenfolge können Testergebnisse verfälscht werden.

Als Beispiel dient das folgende Programm, das zwei Zahlen, die nacheinander eingegeben werden, auf ``Perfektheit'' überprüft:

   1 #!/usr/bin/perl
   2 use strict;
   3 use warnings;
   4
   5 my $tmp;
   6
   7 for(0..1){
   8     print 'Zahl eingeben: ';
   9     my $number = <STDIN>;
     10    
     11     for my $f(1..$number-1){
     12         next unless(($number/$f) == int($number/$f));
     13         $tmp += $f;
     14     }
     15    
     16     print "Perfekte Zahl ? ", $tmp == $number ? "Ja" : "Nein";
     17     print "\n";
     18 }

Wenn erst die 6 und dann die 8 eingeben wird, erhält man folgendes Ergebnis:

  ~/entwicklung 296> perl cgi_test.pl           
  Zahl eingeben: 6
  Perfekte Zahl ? Ja
  Zahl eingeben: 8
  Perfekte Zahl ? Nein

Sieht ja echt gut aus. Mein Programm funktioniert - zumindest scheint es zu funktionieren. Tauscht man die Zahlen, bekommt man folgendes Ergebnis:

  ~/entwicklung 298> perl cgi_test.pl
  Zahl eingeben: 8
  Perfekte Zahl ? Nein
  Zahl eingeben: 6
  Perfekte Zahl ? Nein

Uuups, funktioniert doch nicht. Dieses kleine Beispiel soll zeigen, dass unterschiedliche Testreihenfolgen sehr wichtig sein können.

Zwischenfazit

Die einleitenden Abschnitte sollten zeigen wie wichtig Tests sind. Es möchte sich ja auch niemand in ein Auto setzen, wenn vorher keine Sicherheitstests durchgeführt wurden. Die folgenden Punkte sollen einen kleinen Überblick geben, warum Tests wichtig sind.

Alle sprechen die gleiche Sprache - TAP

Bei Perl hat sich mittlerweile ein Protokoll etabliert, das bei (nahezu) allen Test-Modulen verwendet wird - TAP. TAP steht hierbei für Test Anything Protocol. Damit wird sichergestellt, dass die Ergebnisse von Tests immer gleich dargestellt werden.

Eine Beipielausgabe eines Tests sieht so aus:

  ~/EigeneModule/My-Module 89> perl -Ilib t/test.t 
  1..373
  ok 1 - use My::Module;
  ok 2 - The object isa My::Module
  ok 3 - My::Module->can(...)
  ok 4
  ok 5
  [..]
  ok 17
  ok 18
  ok 19 - F checked hydro
  ok 20 - F checked waal
  ok 21 - F checked iso
  ok 22 - A checked hydro
  ok 23 - A checked waal
  ok 24 - A checked iso

In der ersten Zeile ist der Plan aufgeführt. Was der Plan ist, wird bei Test::Simple genauer erläutert. In den darauf folgenden Zeilen steht für jeden einzelnen Test, der in test.t gemacht wird, das Ergebnis. Mit ok wird gezeigt, dass der Test erfolgreich war und mit not ok dass der Test fehlgeschlagen ist. Die Zahl nach ok beziehungsweise not ok zeigt, welcher Test gelaufen ist.

Bei den meisten Tests kann man noch einen Namen oder eine Nachricht angeben. Damit kann der Test leichter identifiziert werden wenn etwas schiefgelaufen ist. Wenn zum Beipiel Test 3 fehlschlägt, kann man anhand der Nachricht (``My::Module->can(...)'') schnell feststellen, dass bei dem can_ok-Test (siehe Test::More) etwas schiefgelaufen ist und kann sich bei der Fehlersuche auf diesen einen Test beschränken.

Der ganze Aufbau von TAP kann unter perldoc Test::Harness eingesehen werden.

Das Test Anything Protocol wird nicht nur bei Perl verwendet. Es gibt einige Implementierungen für andere Sprachen wie python, PHP und C++.

Parsen des Protokolls

Seit 2006 gibt es auch einen Parser, der dieses Protokoll parsen kann: TAPx::Parser. Das Modul wurde von Curtis 'Ovid' Poe geschrieben und ermöglicht es, die Ausgaben der Testsuite zu parsen und gegebenenfalls weiterzuverarbeiten. Denkbar ist es, die Test-Ergebnisse für eine Webseite aufzubereiten.

Testen von Modulen

Die in diesem Abschnitt beschriebenen Module werden meistens für Tests von anderen Modulen eingesetzt. Sie können aber auch bei Tests von Applikationen eingesetzt.

Test::Harness

Bei Software-Tests ist test harness eine Sammlung von Programmen und Testdaten um ein Programm zu testen. Dies geschieht unter wechselnden Bedingungen und dabei werden die Ergebnisse kontrolliert. Das Perl-Modul Test::Harness übernimmt genau diese Aufgaben: Es startet die Testprogramme und erstellt eine Statistik für die Ergebnisse.

Das Modul bietet auch nur zwei Funktionen:

runtests startet automatisch alle Testskripte, die der Funktion übergeben werden. Ein Beispiel:

  1 #!/usr/bin/perl
  2 use Test::Harness;
  3
  4 my $file = 'example.t';
  5 runtests($file);

Wenn example.t so aussieht:

  1 #!/usr/bin/perl
  2 use Test::More tests => 1;
  3 ok(1 + 1 == 2);

Erhält man folgende Ausgabe:

  C:\community>test_harness.pl
  example....ok  
  All tests successful
  Files=1, Tests=1,  0 wallclock secs ( 0.00 cusr +  0.00 csys =  0.00 CPU)

Nach dem Start des Programms wird eine Liste ausgegeben, in der für jedes gestartete Testskript angezeigt wird, ob die Tests erfolgreich waren oder nicht. Nach dieser Liste wird die Statistik ausgegeben. In diesem Fall waren alle Tests erfolgreich. Hier wurde 1 Programm gestartet, das einen Test enthielt.

Ein zweites Testskript wird der Testsuite hinzugefügt. Dieses Skript enthält auch wieder einen einfachen Test.

  1 #!/usr/bin/perl
  2 use Test::More tests => 1;
  3 ok(1 + 1 == 3);

Dann sollen beide Testskripte gestartet werden:

  1 #!/usr/bin/perl
  2 use Test::Harness;
  3
  4 my @files = qw(harness_bsp.pl harness_bsp2.pl);
  5 runtests(@files);

Es ist leicht zu erkennen, dass der Test im zweiten Skript fehlschlagen wird. Dieser Test wurde eingefügt, um die Statistik zu zeigen wenn ein Test fehlschlägt. Die Ausgabe bei einem Testlauf sieht dann wie folgt aus:

  ~/entwicklung 33> perl harness_test.pl 
  harness_bsp.....ok                                                           
  harness_bsp2....
  #   Failed test in harness_bsp2.pl at line 3.
  harness_bsp2....NOK 1# Looks like you failed 1 test of 1.                    
  harness_bsp2....dubious                                                      
          Test returned status 1 (wstat 256, 0x100)
  DIED. FAILED test 1
          Failed 1/1 tests, 0.00% okay
  Failed Test     Stat Wstat Total Fail  Failed  List of Failed
  -------------------------------------------------------------------------------
  harness_bsp2.pl    1   256     1    1 100.00%  1
  Failed 1/2 test scripts, 50.00% okay. 1/2 subtests failed, 50.00% okay.

Schon während die Skripte laufen, werden die Tests angezeigt, die fehlschlagen. Hier wird durch das NOK 1 angezeigt, dass in dem Skript harness_bsp2 der Test Nummer 1 fehlschlägt. Ganz unten erscheint dann die Aufstellung, welche Tests in welchen Skripten fehlgeschlagen sind. Hier wird harness_bsp2.pl aufgeführt und am Ende wird auch eine Liste der fehlgeschlagenen Tests ausgegeben.

Zum Schluss wird eine Zusammenfassung ausgegeben, in der aufgeführt wird, dass 1 von 2 Skripte fehlgeschlagen sind und dass 1 von 2 Subtests nicht erfolgreich waren.

Die Funktion execute_tests eignet sich, wenn man eine eigene Statistik erstellen will. Sie liefert zwei Hashreferenzen. Im ersten Hash sind alle Daten zu allen Tests: Wie viele Dateien enthalten sind, wie viele Tests geplant sind, wie viele davon erfolgreich oder fehlgeschlagen sind und noch weitere Informationen. Im zweiten Hash befinden sich die Informationen zu den fehlgeschlagenen Tests. Nach Testskript aufgeschlüsselt finden sich Informationen wie: Welche Tests sind fehlgeschlagen, wie viele Tests geplant waren und noch mehr.

Allerdings ist diese Funktion erst ab Version 2.57 implementiert und mit Perl wird erst aber der Version 5.9.4 mit einer Version > 2.57 ausgeliefert.

Weiterhin gibt es noch drei Parameter zur Konfiguration des Moduls:

Wenn $Test::Harness::verbose auf einen ``wahren'' Wert gesetzt wird, wird die Ausgabe der Testskripte mit ausgegeben. Ansonsten werden nur Daten von STDERR ausgegeben.

Mit $Test::Harness::switches kann man Parameter für den Perl-Interpreter setzen. Standardmäßig wird -w gesetzt.

Wenn $Test::Harness::Timer gesetzt wird und Time::HiRes installiert ist, wird nach jedem Testskript die Laufzeit ausgegeben.

Test::Simple

Der Name ist Programm: Simple. Das Modul ist sehr übersichtlich und bietet nur zwei Funktionalitäten, die für einfache Tests ausreichend sind. Dieses Modul beschränkt sich auf zwei grundlegende Funktionen, die auch in Test::More umgesetzt sind:

Es muss ein Plan angegeben werden, der angibt wie viele Test gemacht werden und ist somit ein Test in sich.

Sonst gibt es nur noch die Funktion ok, mit der einfache Tests gemacht werden können. Als ersten Parameter erwartet die Funktion einen boolschen Wert. Ist der Wert wahr, wird ok ausgegeben, andernfalls ein not ok. Als zweiten Parameter kann ein Name für den Test angegeben werden.

Beispiel:

   1 #!/usr/bin/perl
   2 
   3 use strict;
   4 use warnings;
   5 use Test::Simple tests => 5;
   6 
   7 ok(1,'erfolgreicher Test');
   8 ok(0,'nicht erfolgreicher Test');
   9 ok(1 == 1,'1 gleich 1');
     10 ok(get_number() == 23,'get_number() liefert 23');
     11 ok(get_string() eq get_hello(),'get_string() equals get_hello()');
     12 
     13 sub get_number{
     14     23;
     15 }
     16 
     17 sub get_string{
     18     'hello world!'
     19 }
     20 
     21 sub get_hello{
     22     'hello world!'
     23 }

Ausgabe:

  ~/entwicklung 234> perl test_foo.pl 
  1..5
  ok 1 - erfolgreicher Test
  not ok 2 - nicht erfolgreicher Test
  #   Failed test 'nicht erfolgreicher Test'
  #   in test_foo.pl at line 8.
  ok 3 - 1 gleich 1
  ok 4 - get_number() liefert 23
  ok 5 - get_string() equals get_hello()
  # Looks like you failed 1 test of 5.

Test::More

Test::More ist so ziemlich das wichtigste Modul bei Tests. Es bietet einige Funktionen, die Tests erleichtern. Test::More erspart umständliche Aufrufe der ok-Funktion wie es in Test::Simple notwendig ist. Die ok-Funktion existiert zwar weiterhin, aber in den meisten Fällen ist eine andere Funktion besser.

Wichtig - wie bei Test::Simple - ist es, einen Plan festzulegen. Über den Plan teilt man Test::More mit, wie viele Tests gemacht werden sollen. Das ist ein Test in sich, da der Tester eine Meldung bekommt, wenn in dem Testskript mehr oder weniger Tests als angegeben durchgeführt wurden. Das kann darauf hindeuten, dass einige Tests vergessen wurden oder dass mehr Tests als nötig gemacht wurden.

Bei jedem Test, der geschrieben wird, muss also der Plan angepasst werden.

In den nachfolgenden Abschnitten werden einige Funktionen von Test::More besprochen.

Bei allen Tests, die bei Test::More gemacht werden, testen das folgende Modul:

   1 package FooBar;
   2 
   3 sub return_42{
   4     return 42;
   5 }
   6 
   7 sub echo{
   8     my ($echo) = @_;
   9     return $echo;
     10 }
     11
     12 sub random{
     13     my $text = 'Dies ist die Zufallszahl ' . rand($$);
     14     return $text;
     15 }
     16
     17 sub lower_case{
     18     my ($echo) = @_;
     19     $echo = lc $echo;
     20     return $echo;
     21 }
     22 
     23 sub get_title{
     24     my ($html) = @_;
     25     my ($title) = $html =~ m!<title>(.*?)</title>!;
     26     return $title;
     27 }
     28 
     29 sub todo{
     30 }
     31
     32 1;

Es ist natürlich möglich, die ok-Funktion zu verwenden, wie sie aus Test::Simple bekannt ist. In den meisten Fällen soll aber vermutlich etwas verglichen werden; entweder zwei Strings oder zwei Zahlen. Die erste Methode, die das Leben einfacher macht, ist cmp_ok. Diese Funktion benötigt drei Parameter plus den optionalen Test-Namen. Noch einfacher ist es allerdings mit der is-Funktion. Diese Funktion erkennt automatisch, ob Zahlen oder Strings verglichen werden sollen und macht den nötigen Abgleich.

Die drei folgenden Tests sind also äquivalent:

  1 #!/usr/bin/perl
  2
  3 use Test::More tests => 3;
  4 use FooBar;
  5
  6 my $number = 42;
  7 ok(FooBar::return_42() == 42);         # ok
  8 cmp_ok(FooBar::return_42(), '==', 42); # ok
  9 is(FooBar::return_42(),42);            # ok

Genauso wie:

  1 #!/usr/bin/perl
  2
  3 use Test::More tests => 3;
  4 use FooBar;
  5 
  6 my $string = 'Test';
  7 ok(FooBar::echo($string) eq $string);         # ok
  8 cmp_ok(FooBar::echo($string), 'eq', $string); # ok
  9 is(FooBar::echo($string),$string);            # ok

Für die Funktion is gibt es auch noch das Gegenteil und heißt sinnigerweise isnt. Damit können Vergleiche auf Ungleichheit gemacht werden:

  1 my $string = 'Test';
  2 isnt(FooBar::echo($string),'test'); # ok
  3 is(FooBar::echo($string),'test');   # not ok

Die eben genannten Funktionen prüfen auf Gleichheit. In einigen Fällen ist es aber so, dass man nicht genau weiß, wie etwas zurückgeliefert wird oder der Zufallszahlengenerator ist mit beteiligt - wie bei

  1 sub random{
  2     my $text = 'Dies ist die Zufallszahl ' . rand($$);
  3     return $text;
  4 }

Es ist aber ein Teil des Rückgabewerts bekannt. Im normalen Programm würde man gleich mit Regulären Ausdrücken anfangen. Auch Test::More bietet Funktionen, die mit Regulären Ausdrücken umgehen können: like und das Gegenteil dazu unlike.

Damit kann man auch Funktionen wie das oben beschriebene random testen:

   1 #!/usr/bin/perl
   2
   3 use Test::More tests => 3;
   4 use FooBar;
   5
   6 my $test     = 'Dies ist die Zufallszahl ';
   7 my $test_zwo = 'ein anderer Test ';
   8
   9 like(FooBar::random(),qr/$test/);       # ok
     10 unlike(FooBar::random(),qr/$test/);     # not ok
     11
     12 like(FooBar::random(),qr/$test_zwo/);   # not ok
     13 unlike(FooBar::random(),qr/$test_zwo/); # ok

Bisher wurden nur einfache Tests gemacht: Strings und Zahlen verglichen. In Perl gibt es aber noch andere Datentypen: Arrays und Hashes. Auch die sollen verglichen werden wenn sie der Rückgabewert einer Funktion sind.

Ein einfacher Vergleich wie dieser:

  1 my @array_eins = qw(1 2 3);
  2 my @array_zwo  = qw(1 2 3);
  3 is(\@array_eins,\@array_zwo);

geht schief.

  not ok 1
  #   Failed test in test_foo.pl at line 9.
  #          got: 'ARRAY(0x209624)'
  #     expected: 'ARRAY(0x164e74)'

Da muss also etwas anderes her. Test::More hat auch hier die entsprechende Funktion: is_deeply. Diese Funktion kann aber nur einfache Hashes beziehungsweise Arrays vergleichen. Für komplexe Datenstrukturen sind die Module Test::Differences und Test::Deep geeignet. Aber für Arrays und Hashes ist die is_deeply-Funktion von Test::More sehr gut geeignet:

  1 #!/usr/bin/perl
  2
  3 use Test::More tests => 2;
  4
  5 my @array_eins = qw(1 2 3);
  6 my @array_zwo  = qw(1 2 3);
  7 my @array_drei = qw(1 2 4);
  8 is_deeply(\@array_eins,\@array_zwo);
  9 is_deeply(\@array_eins,\@array_drei);

Ergibt folgende Ausgabe:

  ~/entwicklung 127> perl test_foo.pl 
  1..2
  ok 1
  not ok 2
  #   Failed test in test_foo.pl at line 13.
  #     Structures begin differing at:
  #          $got->[2] = '3'
  #     $expected->[2] = '4'
  # Looks like you failed 1 test of 2.

Es wird also auch gut aufgezeigt, an welcher Stelle etwas schief gelaufen ist.

An der Testausgabe sieht man, dass es auch Zusatzinformationen zu einem Test gibt. In diesem Fall wird angegeben, welche Elemente der Arrays ungleich sind. Solche zusätzlichen Ausgaben - die von Test::Harness übrigens ignoriert werden - können auch selbst ausgegeben werden. Dafür gibt es die Funktion diag:

  1 #!/usr/bin/perl
  2
  3 use Test::More tests => 1;
  4 
  5 diag('Diagnosemeldung: Test1');
  6 is(42,42);

Und die Ausgabe:

  ~/entwicklung 128> perl test_foo.pl 
  1..1
  # Diagnosemeldung: Test1
  ok 1

Für die Überprüfung, ob alle gewünschten Funktionen in einem Modul implementiert sind, gibt es die Funktion can_ok. Die Funktion akzeptiert sowohl ein Objekt als auch ein Modulname für den Test:

   1 #!/usr/bin/perl
   2 
   3 use strict;
   4 use warnings;
   5 use Test::More tests => 1;
   6 use FooBar;
   7
   8 my @methods = qw(echo return_42
   9                  lower_case random
     10                  get_title todo);
     11 
     12 can_ok('FooBar',@methods);

Da alle diese Methoden in dem Modul implementiert sind, bekommt erscheint diese Ausgabe:

  ~/entwicklung 129> perl test_foo.pl 
  1..1
  ok 1 - FooBar->can(...)

Für Objektorientierte Programmierung gibt es noch die Funktion isa_ok, die ein Objekt überprüft, ob es zu einer gegebenen Klasse gehört:

  1 #!/usr/bin/perl
  2 
  3 use Test::More tests => 1;
  4 use CGI;
  5 
  6 my $cgi = CGI->new();
  7 isa_ok($cgi,'CGI');

Da $cgi ein Objekt con CGI ist, erhält man folgende Ausgabe:

  ~/entwicklung 130> perl test_foo.pl 
  1..1
  ok 1 - The object isa CGI

Soll das Laden von Modulen getestet werden, stehen zwei Funktionen aus Test::More zur Verfügung: use_ok und require_ok. Die Namen sind dabei Programm: use_ok bindet ein Modul über use ein und das require_ok benutzt das require.

Mit folgendem Skript wird getestet ob zwei bestimmte Module installiert sind:

  1 #!/usr/bin/perl
  2 
  3 use strict;
  4 use warnings;
  5 use Test::More tests => 2;
  6
  7 use_ok('CGI');
  8 use_ok('NonExistentModule');

Da das zweite Modul nicht installiert ist, erscheint folgende Ausgabe:

  ~/entwicklung 154> perl test_foo.pl 
  1..2
  ok 1 - use CGI;
  not ok 2 - use NonExistentModule;
  #   Failed test 'use NonExistentModule;'
  #   in test_foo.pl at line 8.
  #     Tried to use 'NonExistentModule'.
  #     Error:  Can't locate NonExistentModule.pm 
    in @INC (@INC contains: ... .) at (eval 4) line 2.
  # BEGIN failed--compilation aborted at test_foo.pl line 8.
  # Looks like you failed 1 test of 2.

Die letzten beiden Punkte von Test::More sind der SKIP- und der TODO-Block. Mit dem SKIP-Block kann man einen Teil des Codes auslassen wenn eine bestimmte Bedingung erfüllt ist. Dies ist sehr nützlich, wenn für den Test eine Internetverbindung benötigt wird, bei einer nicht vorhandenen Internetverbindung nicht alle Tests fehlschlagen sollen. Sonst begibt man sich auf Fehlersuche und dabei lag es nur an der fehlenden Verbindung.

Ein weiterer - noch häufigerer - Anwendungsfall ist das Abfragen von Modulen. Wenn für Tests bestimmte Module benötigt werden, die auf dem Rechner vielleicht nicht installiert sind, macht es keinen Sinn, den Test weiter auszuführen.

Die folgenden zwei SKIP-Blöcke sollen dies demonstrieren:

   1 #!/usr/bin/perl
   2
   3 use strict;
   4 use warnings;
   5 use Test::More tests => 2;
   6 use LWP::Simple;
   7 
   8 SKIP:{
   9     eval "use NonExistentMod";
     10     skip "NonExistentMod not installed",1 if $@;
     11     
     12     NonExistentMod::subroutine();
     13 }
     14 
     15 SKIP:{
     16     my $content = get('http://www.1.de/');
     17     skip "Keine Verbindung",1 unless $content;
     18     
     19     require FooBar;
     20     is(FooBar::get_title($content),'Test');
     21 }

Da beides nicht funktioniert, wird folgende Meldung ausgegeben:

  ~/entwicklung 161> perl test_foo.pl 
  1..2
  ok 1 # skip NonExistentMod not installed
  ok 2 # skip Keine Verbindung

Mit dem TODO-Block kann angegeben werden, welche Tests fehlschlagen werden, weil die Funktionalität noch nicht implementiert ist. In dem Beispielmodul soll die Funktion todo in Zukunft mal den Satz "Hallo Welt!" zurückliefern. Der Funktionsrahmen ist zwar schon aufgeschrieben, die Funktion ist aber noch nicht mit Leben gefüllt.

So könnte also ein Test aussehen:

   1 #!/usr/bin/perl
   2
   3 use strict;
   4 use warnings;
   5 use Test::More tests => 1;
   6 use FooBar;
   7 
   8 TODO:{
   9     local $TODO = 'not yet implemented';
     10     is(FooBar::todo(),'Hallo Welt!','teste todo()');
     11 }

Die Testausgabe sieht dann so aus:

  ~/entwicklung 243> perl test_foo.pl 
  1..1
  not ok 1 - teste todo() # TODO not yet implemented
  #   Failed (TODO) test 'teste todo()'
  #   in test_foo.pl at line 10.
  #          got: undef
  #     expected: 'Hallo Welt!'

Wenn eine Testsuite mit Test::Harness gestartet wird, werden diese Tests im TODO-Block nicht als fehlgeschlagen gewertet.

Test::Exception

Mit Test::Exception können Module auf Warnungen und Fehler hin überprüft werden. Damit kann zum Beispiel überprüft werden, ob das Modul tatsächlich abbricht wenn eine nicht-existente Datei geöffnet werden soll.

Das Modul stellt die folgenden vier Methoden zur Verfügung.

Mit dies_ok überprüft man, ob eine Funktion wie gewünscht abbricht. Programme liefern häufig falsche Ergebnisse wenn bei bestimmten Ereignissen nicht abgebrochen wird. Deshalb sollte ein Programm mit wissentlich falschen Daten gefüttert werden, um zu überprüfen, ob das Programm richtig darauf reagiert.

throws_ok überprüft auch, ob die Fehlermeldung durch einen bestimmten Regulären Ausdruck gematcht wird. Dies ist sinnvoll, wenn nicht nur überprüft werden soll, ob das Programm abbricht, sondern auch ob der ``richtige'' Fehler ausgegeben wird.

Eine Methode, die auf jeden Fall durchläuft, kann mit lives_ok getestet werden. Hierbei ist es völlig egal, welchen Rückgabewert die Methode hat. Nicht egal ist dies wiederum bei lives_and. Hier wird noch ein zusätzlicher Test auf den Rückgabewert gemacht.

Dieses Modul soll getestet werden:

   1 package FooBar;
   2 
   3 sub dies{
   4     open my $fh,'<','/dies/ist/ein/test.upc' or die $!;
   5     close $fh;
   6 }
   7 
   8 sub throws{
   9     my ($nenner) = @_;
     10     my $zahl = 19 / $nenner;
     11 }
     12 
     13 sub lives{
     14     1;
     15 }
     16 
     17 sub lives_42{
     18     42;
     19 }
     20 
     21 1;

Folgendes Testskript benutzt die vier Methoden von Test::Exception um den Einsatz des Moduls zu verdeutlichen:

   1 #!/usr/bin/perl
   2
   3 use strict;
   4 use warnings;
   5 use Test::More tests => 4;
   6 use Test::Exception;
   7 use FooBar;
   8 
   9 dies_ok   {FooBar::dies()           } 
     10               'died as expected';
     11 throws_ok {FooBar::throws(0)        } 
     12               qr/division by zero/i, 
     13               'Division by 0 not allowed';
     14 lives_ok  {FooBar::lives()          }
     15               'it does not matter what lives() returns';
     16 lives_and {is FooBar::lives_42(), 42} 
     17               'lives_42 returns 42';

Die Ausgabe des Tests sieht wie folgt aus:

  ~/entwicklung 79> perl test_foo.pl 
  1..4
  ok 1 - died as expected
  ok 2 - Division by 0 not allowed
  ok 3 - it does not matter what lives() returns
  ok 4 - lives_42 returns 42

Test::TestCoverage

Es wurde von mehreren Personen angemerkt, dass Test::TestCoverage sehr dem Modul Devel::Cover ähnelt. Es gibt Ähnlichkeiten im Sinn und Zweck der Module, aber Test::TestCoverage verfolgt eine etwas andere Strategie.

Test::Coverage vs Devel::Cover

Mit Devel::Cover kann überprüft werden, wie häufig bestimmt Code-Stücke aufgerufen wurde. So kann man feststellen, ob unnützer Code geschrieben wurde. Test::TestCoverage interessiert sich nicht dafür, wie oft ein Code-Stück aufgerufen wurde und erzeugt auch nicht den Overhead an Statistiken. Test::TestCoverage interessiert sich nur dafür, ob auch tatsächlich alle ``public''-Methoden im Testskript aufgerufen wurden.

Nachfolgend werden die Funktionen von Test::TestCoverage erläutert.

Mit test_coverage wird ein Modul zur Überprüfung ``angemeldet''. Wenn das Modul noch nicht geladen ist, dann wird es automatisch geladen. Dies ist auch notwendig, damit Test::TestCoverage die Subroutinen des Moduls herausfindet.

Der eigentliche Test ist ok_test_coverage. Standardmäßig wird dabei das zuletzt angemeldete Modul überprüft. Soll ein anderes Modul getestet werden, muss der Name als Parameter übergeben werden.

Wurden mehrere Module ``angemeldet'', kann man die Tests auch vereinfachen und über all_test_coverage_ok alle Module auf einmal testen.

Als Beispielmodul wird folgendes Modul genommen:

  1 package FooBar;
  2
  3 sub new{ bless {},shift }
  4
  5 sub echo{
  6     my ($self,$echo) = @_;
  7     print $echo,"\n";
  8 }
  9 1;

Dieses Modul soll getestet werden und bei dem Test soll darauf geachtet werden, dass alle public-Methode verwendet werden (hier: new und echo). Das Testskript sieht dann wie folgt aus:

   1 #!/usr/bin/perl
   2
   3 use strict;
   4 use warnings;
   5 use Test::More tests=>1;
   6 use Test::TestCoverage;
   7
   8 test_coverage('FooBar');
   9 
     10 my $foo = FooBar->new();
     11 
     12 ok_test_coverage();

Wenn der Test so läuft, erhält man folgende Ausgabe:

  ~/entwicklung 67> perl test_foo.pl 
  1..1
  not ok 1 - Test test-coverage echo  are missing
  #   Failed test 'Test test-coverage echo  are missing'
  #   in test_foo.pl at line 12.
  #          got: 0
  #     expected: 1
  # Looks like you failed 1 test of 1.

Damit wird angezeigt, dass die Methode echo nicht aufgerufen wird. Fügt man einen Aufruf der Methode ein, so sieht das Testskript folgendermaßen aus:

   1 #!/usr/bin/perl
   2
   3 use strict;
   4 use warnings;
   5 use Test::More tests=>1;
   6 use Test::TestCoverage;
   7
   8 test_coverage('FooBar');
   9 
     10 my $foo = FooBar->new();
     11 $foo->echo('Hallo Welt');
     12 
     13 ok_test_coverage();

Jetzt läuft der Test fehlerfrei durch:

  ~/entwicklung 69> perl test_foo.pl 
  1..1
  Hallo Welt
  ok 1 - Test test-coverage

Das Modul hat noch einige Schwächen, da zum Beispiel exportierte Methoden noch nicht überprüft werden können. Da ist Devel::Cover schon einiges weiter.

Test::CheckManifest

Dieses Modul ist eigentlich kein Test-Modul um Funktionalität zu sichern, sondern unterstützt den Programmierer bei der Einhaltung von CPAN-Konformität. In der MANIFEST-Datei sind alle Dateien aufgeführt, die zu einer Distribution gehören - zumindest sollte es so sein.

Ob dies auch tatsächlich der Fall ist, kann sehr einfach mit Test::CheckManifest überprüft werden. Ein

  1 use Test::More tests => 1;
  2 
  3 SKIP:{
  4     eval "use Test::CheckManifest 1.0";
  5     skript "Test::CheckManifest 1.0 required",1 if $@;
  6     
  7     ok_manifest();
  8 }

ist ausreichend.

Die Methode ok_manifest ist die einzige Methode, die es in dem Modul gibt.

Der Methode kann aber in einer Hashreferenz ein Filter und Informationen über Verzeichnisse mitgegeben werden, die aus dem Test ausgeschlossen werden sollen.

So kann mit

  ok_manifest({filter => [qr/\.svn/]});

ein Filter übergeben werden, der alle Dateien von dem Test ausschließt, die ein .svn im Namen (oder im Pfadnamen) haben.

Mit

  ok_manifest({exclude => ['/.svn']});

werden alle Dateien aus dem .svn-Ordner ausgeschlossen.

Testen der Dokumentation

Ein zweiter Bereich, der gerne vernachlässigt wird, ist die Dokumentation. Bei einem Großteil der Programmierer ist dieser Teil vermutlich noch unbeliebter als das Testen von Software. Doch Dokumentation ist wichtig, um Kunden einen Einblick in die Software zu gewähren. Die Dokumentation ist aber auch entscheidend für die Wartbarkeit eines Programms: Ist das Programm nicht dokumentiert, so fällt es häufig schwer, nach drei oder vier Monaten das Programm zu erweitern. ``Wie war das nochmal? Was macht der Codeteil?'' - so oder so ähnliche Sätze hört man dann vor den Bildschirmen.

Um sich selbst und andere Programmierer zu etwas Disziplin zu ``zwingen'', erlaubt es Perl, die Dokumentation zu überprüfen. Es dreht sich hier um die Dokumentation in POD. In den folgenden Sektionen werden zwei Module beschrieben, die für das Testen der Dokumentation verwendet werden können.

Test::Pod

Das erste Modul zum Testen der Dokumentation ist Test::Pod. Es überprüft die POD-Dokumentation auf Korrektheit und vor allem, ob überhaupt etwas da ist! Die Dokumentation wird darauf überprüft, ob sie der Syntax entspricht, die in perldoc perlpod beschrieben ist.

Es wird allerdings nicht überprüft, ob das Programm vollständig dokumentiert ist.

Das Modul exportiert drei Methoden in das Testskript:

Test::Pod::Coverage

Testen von Konsole-Applikationen

Im vorangegangenen Abschnitt wurden die Module beschrieben, die hauptsächlich bei Tests von Modulen eingesetzt werden. Die Test-Strategie sollte jedoch weitergeführt werden und auch auf Applikationen angewendet werden. Dieser Abschnitt und die beiden folgenden Abschnitte zeigen, wie man auch Skripte und andere Applikationen testen kann.

Test::Cmd

Test::Cmd ist ein kleines Modul, mit dem man Konsolen-Applikationen testen kann. Es ist etwas gewöhnungsbedürftig, ist jedoch geeignet, um Programme zu testen. Das Modul

Ein einfaches Skript, das getestet werden soll:

   1 #!/usr/bin/perl
   2
   3 use strict;
   4 use warnings;
   5
   6 print "Geben Sie eine Zahl ein: ";
   7 chomp(my $zahl = <STDIN>);
   8 
   9 print '>>',$zahl,"<<\n";

Es gibt einfach die Zahl, die eingegeben wurde, umrahmt von ``>>'' und ``<<'' aus.

Getestet wird es mit dem folgenden Skript:

   1 #!/usr/bin/perl
   2
   3 use strict;
   4 use warnings;
   5 use Test::Cmd;
   6 use Test::More tests => 12;
   7
   8 my $test = Test::Cmd->new(prog => 'cmd_bsp.pl', 
   9                           workdir => './testing') or die $!;
     10 $test->interpreter('perl');
     11 
     12 for(1..12){
     13     my $nr = rand 2998;
     14     $test->run(stdin => $nr);
     15     my $result = $test->stdout();
     16     like($result,qr/>>$nr<<$/);
     17 }

Ich überprüfe hier, ob das Programm mir auch tatsächlich alle möglichen Eingaben wieder ausgibt. Test::Cmd bietet verschiedene Einstellmöglichkeiten zum Testen. Man kann dem zu testenden Programm auch Kommandozeilenargumente übergeben und so verschiedene Einstellungen testen.

Der Test zeigt, dass das Programm alles richtig macht:

  ~/entwicklung 28> perl test_cmd.pl 
  1..12
  ok 1
  ok 2
  ok 3
  ok 4
  ok 5
  ok 6
  ok 7
  ok 8
  ok 9
  ok 10
  ok 11
  ok 12

Das Modul legt ein temporäres Verzeichnis an, in dem verschiedene Dateien zwischengespeichert werden. Dort werden für die verschiedenen Durchläufe zum Beispiel die Ausgaben des getesteten Programms und die Ausgaben auf STDERR in Dateien gespeichert, damit diese Informationen später wieder abgerufen werden können.

So sieht es beispielhaft in dem temporären Verzeichnis aus:

  ~/entwicklung 4> ll testing
  total 2
  -rw-rw-r--   1 reneeb   bioinf         0 Oct 11 13:32 stderr.1
  -rw-rw-r--   1 reneeb   bioinf         0 Oct 11 13:32 stderr.2
  -rw-rw-r--   1 reneeb   bioinf       136 Oct 11 13:32 stdout.1
  -rw-rw-r--   1 reneeb   bioinf         0 Oct 11 13:32 stdout.2

Bei Beendigung des Testskripts räumt das Modul wieder auf.

Das Modul geht relativ strikt vorwärts. Zum Überprüfen, ob das Skript durchgelaufen ist, oder ob es abgebrochen wurde, stehen die Funktionen pass beziehungsweise fail zur Verfügung. Das Modul liefert leider keine Funktionen, um die Ausgaben des getesteten Programms zu überprüfen. Das muss man dann mit Test::More und dessen Funktionen machen (wie im Beispielskript).

Test::Expect

Ein weiteres Modul zum Testen von Konsole-Applikationen ist Test::Expect. Dafür ist es jedoch notwendig, dass das zu testende Programm einen Prompt hat. Dadurch erkennt das Modul, wann es Eingaben senden kann. Zusätzlich muss das Programm durch einen Befehl zu Beenden sein, da sonst das Testskript hängt wenn noch Eingaben fehlen. Aus diesem Grund wurde obiges Beispielskript erweitert und sieht somit folgendermaßen aus:

   1 #!/usr/bin/perl
   2 
   3 use strict;
   4 use warnings;
   5 
   6 for(0..2){
   7     print "Geben Sie eine Zahl ein: ";
   8     chomp(my $zahl = <STDIN>);
   9     if($zahl eq 'quit'){
     10          exit;
     11     }
     12     print '>>',$zahl,"<<\n";
     13     sleep 2;
     14 }

Der Test mit Test::Expect sieht dann so aus:

   1 #!/usr/bin/perl
   2
   3 use strict;
   4 use warnings;
   5 use Test::Expect;
   6 use Test::More tests => 3;
   7
   8 expect_run(
   9     command => 'perl cmd_bsp.pl',
     10     prompt  => 'Geben Sie eine Zahl ein: ',
     11     quit    => 'quit',
     12 );
     13
     14 expect(3,'>>3<<');

Ohne die eingebaute Abfrage nach quit würde das Testskript stehen bleiben und darauf warten, dass es etwas an das zu testende Skript senden kann. Die Dokumentation zu Test::Expect ist leider sehr rudimentär. So ist an keiner Stelle vermerkt, dass man bei Test::More zwei Tests pro expect-Aufruf rechnen muss und dass expect_run auch einen Test beinhaltet. Sieht man das Testskript, so erwartet man erstmal nur einen Test: bei dem expect-Aufruf.

Dann sieht die Ausgabe aber wie folgt aus:

  ~/entwicklung 31> perl test_expect.pl 
  1..1
  ok 1 - expect_run
  ok 2
  ok 3
  # Looks like you planned 1 test but ran 2 extra.

An der Ausgabe kann man dann erkennen, dass expect_run einen Test beinhaltet. Ein Blick in den Quellcode des Moduls offenbart dann auch, wo der zweite zusätzliche Test herkommt: expect ruft intern zwei Funktionen auf, die jeweils einen Test machen.

Zwischenfazit ``Konsole-Applikationen''

Mit beiden Modulen lassen sich Konsolen-Anwendungen testen. Allerdings ist die Verwendung der Module nicht ganz so intuitiv wie bei anderen Test-Modulen. Das sollte aber keinen abhalten, solche Anwendungen zu testen.

Die Dokumentation von beiden Modulen ist nicht besonders gut.

Test::Cmd hat weniger Abhängigkeiten und lässt sich leichter benutzen.

Testen von GUI-Applikationen

Viele Programme sind keine Konsolen-Programme sondern Programme mit Graphischer Oberfläche. Diese sind vielleicht mit Perl/Tk, WxPerl oder einer ganz anderen Sprache entwickelt. Auch GUIs sollten getestet werden. Ist beim Menü überall die richtige Funktion hinterlegt? Machen die Funktionen das was sie tun sollen?

X11::GUITest

Mit X11::GUITest können Applikationen mit Graphischen Oberflächen (GUI) getestet werden, die ein X11-Framework verwenden. Diese Frameworks sind zum Beispiel GTK+,Qt,Xt und Motif. Es gibt aber noch weitere Frameworks.

Für das Modul X11::GUITest stehen nicht so viele Funktionen zur Verfügung wie für Win32::GuiTest.

Da Tastatureingaben immer an das aktive Fenster geschickt werden, muss das Programm im aktiven Workspace laufen und es darf zwischendurch kein anderes Fenster aktiviert werden. Es wäre wünschenswert, wenn man Tastatureingaben an ein ausgewähltes Fenster schicken könnte.

Unter Solaris ist folgender Test erfolgreich gelaufen:

Das Skript test.pl erzeugt ein Fenster mit Perl/Tk und wenn der Anwender Alt-C drückt, wird eine Datei erzeugt. Das Skript sieht wie folgt aus.

   1 #!/usr/bin/perl
   2
   3 use strict;
   4 use warnings;
   5 use Tk;
   6 
   7 my $mw = tkinit();
   8 $mw->bind('<Alt-c>',\&test);
   9 
     10 MainLoop;
     11 
     12 sub test{
     13     open(my $fh,'>','test.txtt') or die $!;
     14     print $fh 'FooBar';
     15     close $fh;
     16
     17     exit;
     18 }

Das Skript leistet nichts großartiges, soll aber ein Beispiel sein, wie man mit X11::GUITest die Funktionalität einer GUI überprüfen kann. Doch wie testet man jetzt diese GUI? Auch hier reicht ein kleines Skript.

   1 #!/usr/bin/perl
   2
   3 use strict;
   4 use warnings;
   5 use X11::GUITest qw(StartApp SendKeys FindWindowLike);
   6
   7 # starte die GUI
   8 StartApp('/pfad/zu/test.pl');
   9 
     10 # Zeit lassen zu Starten
     11 sleep(2);
     12
     13 # Der Titel wird von Perl/TK gesetzt wenn nicht explizit angegeben
     14 my ($window) = FindWindowLike('Test');
     15 
     16 if(!$window){
     17     die "Couldn't find a window\n";
     18 }
     19 
     20 # Sende Alt-C
     21 SendKeys('%(c)');
     22 sleep(2);
     23 print 'ok' if(-e 'test.txtt');

Wie man sieht, ist es relativ einfach, eine GUI zu testen.

Win32::GuiTest

Win32::GuiTest ist das Windows-Pendant zu X11::GuiTest. Als Beispiel-Applikation soll der Editor von Herbert Breunung, der Editor ``kephra'', getestet werden.

Wichtig ist, dass man eine sehr neue Version von Win32::GuiTest verwendet, da viele wichtige Funktionen erst in den neuesten Versionen implementiert sind. In dem Test, soll überprüft werden, ob die ``Speichern unter...''-Funktion richtig funktioniert.

Dazu wird nur die Funktion SendKeys benötigt. Das Modul bietet für sehr viele wichtige Funktionen die entsprechenden Methoden.

Das Beispielprogramm sieht dann so aus:

   1 #!/usr/bin/perl
   2
   3 use strict;
   4 use warnings;
   5 use Win32::GuiTest qw(:ALL);
   6
   7 my $dir = 'C:\PCE-0.3\pce';
   8 chdir $dir;
   9
     10 system("start pce.exe");
     11 sleep(2);
     12 my @windows = FindWindowLike(undef,"PCE");
     13
     14 print "Couldn't find a window\n" unless(@windows);
     15 print "more than one window\n" if scalar(@windows) > 1;
     16 
     17 # simulate Ctrl+Shift+S
     18 SendKeys("+^s");
     19 # send PID + '.txt'
     20 SendKeys($$.".txt");
     21 # press <ENTER>
     22 SendKeys("{ENTER}");

In einem Beispiel, in dem die Maus navigiert werden soll, um das Programm über die Toolbar neu zu speichern, wird die Funktion MoveMouseAbsPix benötigt, um die Maus zu einem bestimmten Punkt zu navigieren. Mit SendMouse wird dann der Mausklick simuliert. Das Beispielprogramm sieht folgendermaßen aus:

Für alle Funktionen ein Beispiel zu zeigen, wäre zu aufwendig, weshalb es bei den zwei gezeigten Beispielen bleibt.

Zwischenfazit ``GUI-Testing''

Das GUI-Testing sollte aus mehreren Teilen bestehen: Dem Test des Codes, Test der Oberfläche und dem Usability-Test. Wie man den Code testen kann, ist in den vorangegangenen Abschnitten beschrieben worden. Den Usability-Test kann man nicht wirklich automatisieren. Hier müssen echte Personen ran.

Der Test der Oberfläche dient dazu, dass die Buttons auch das richtige tun und dass die Verknüpfung von Oberfläche und Code das gewünschte Ergebnis erzielt. Diese Tests sollten gemacht werden, denn was bringt schon der beste Code, wenn sich unter dem ``Datei öffnen''-Button die ``Suche''-Funktionalität verbirgt. Und wenn man etwas speichern will, wird auf einmal die Festplatte gelöscht.

Das sind Fehler, die in keinem Programm passieren sollten. Mit Win32::GuiTest und X11::GUITest sind zwei Module vorhanden, die diese Tests der Oberfläche erleichtern.

Testen von Web-Applikationen

Ein weiteres Gebiet, auf dem häufig Tests vernachlässigt werden sind die Web-Applikationen. Noch nicht einmal große Firmen - die es sich leisten könnten und eigentlich von einer funktionierenden Seite profitieren - testen ausreichend ihre Web-Applikationen.

Web-Applikationen sollten neben der Usability auf zwei Punkte hin getestet werden: Die Funktionalität und wie sich die Anwendung unter Last verhält. Bevor Lasttests gemacht werden, sollte erst die Funktionalität überprüfen. Denn was bringt eine Webseite, die gut skaliert aber nicht die gewünschten Funktionen bietet. Für Lasttests gibt es Tools wie JMeter, das in Java geschrieben ist, und openSTA, das es nur für Windows gibt. Perl bietet auch die Möglichkeit, mit HTTP::Recorder Lasttests durchzuführen.

Um die Funktionalität zu überprüfen, gibt es vor allem zwei Module, die dies erleichtern: Test::WWW::Mechanize und Test::WWW::Selenium. Man kann sich die Testsuite auch mit LWP zusammenbauen, aber die zwei genannten Module bieten schon alle wichtigen Funktionen.

In den folgenden Sektionen werden die Module für die Funktionstests und das Modul für Lasttests vorgestellt.

Test::WWW::Mechanize

Test::WWW::Mechanize setzt - wie der Name schon sagt - auf WWW::Mechanize auf. Mit dem Modul kann man sehr leicht die Funktionalität einer Webseite überprüfen und ob immer das richtige Ergebnis geliefert wird.

Man kann natürlich WWW::Mechanize und Test::More selbst kombinieren, aber das macht unnötig Arbeit und wird schnell unübersichtlich.

Mit

  #!/usr/bin/perl
  
  use strict;
  use Test::More tests => 2;
  use Test::WWW::Mechanize;
  my $mechanize = Test::WWW::Mechanize->new();
    
  # check "home"
  $mechanize->get_ok( 'http://www.foo-magazin.de' );
  $mechanize->content_like(qr/Perl/i, "found Perl" );
  $mechanize->title_is( '$foo - Perl-Magazin', 'Perl-Magazin title' );
  
  # check "preis"
  $mechanize->follow_link_ok( {text => 'Preise'}, 'go to prices' );
  $mechanize->content_contains( 'Einzelheft', 'contains price for one magazine' );
  $mechanize->content_contains( '6.00 &euro;', 'and there\'s a price of 6 Euro' );

Wird die Webseite http://www.foo-magazin.de überprüft. Die ersten drei Tests betreffen die Startseite. Hier wird erstmal überprüft, ob eine Antwort kommt. Irgendwo im Seiteninhalt muss dann das Wort ``Perl'' auftauchen. Der dritte Test checkt dann, ob die Seite den richtigen Titel enthält.

Die darauffolgenden drei Tests überprüfen eine weitere Seite und ob man von der Startseite zu der ``Preise''-Seite kommt. Bei den Preisen muss dann ein Preis für ein Einzelheft genannt. Und es muss ein Preis von 6 Euro auftauchen...

Die Ausgabe des Testskripts sieht dann so aus:

  C:\>test_mechanize.pl
  1..6
  ok 1
  ok 2 - found Perl
  ok 3 - Perl-Magazin title
  ok 4 - go to prices
  ok 5 - contains price for one magazine
  ok 6 - and there's a price of 6 Euro

Test::WWW::Selenium

Selenium ist in Java geschrieben - was hat das dann hier zu suchen? Es ist nur der Server in Java geschrieben, der Rest in Perl. Um mit Test::WWW::Selenium zu arbeiten, ist eine relativ neue Version der Java Runtime Environment (JRE) nötig - für neue Versionen von Test::WWW::Selenium ist die JRE 1.5 notwendig.

Im Gegensatz zu Test::WWW::Mechanize wird nicht Perl als Browser verwendet, sondern ein lokal installierter Browser. Dadurch ist es auch möglich, JavaScript zu testen, was bei Test::WWW::Mechanize nicht möglich ist.

Selenium kann mit den gängigsten Browsern arbeiten:

Installation

Bei der Installation müssen zwei Pakete installiert werden: Test::WWW::Selenium und Alien::SeleniumRC. Alien::SeleniumRC enthält die Server-Komponente, und das Test-Modul hält die Funktionen für die Tests vor.

Wenn alles installiert ist, kann es mit dem Testen losgehen. Es soll wieder eine Webseite des Perl-Magazins getestet werden.

   1 #!/usr/bin/perl
   2
   3 use strict;
   4 use warnings;
   5 use Test::More tests => 2;
   6 use Test::WWW::Selenium;
   7
   8 my $url = 'http://www.foo-magazin.de';
   9
     10 my $sel = Test::WWW::Selenium->new(
     11                host => 'localhost',
     12                port          => 4444,
     13                browser       => '*firefox',
     14                browser_url   => $url,
     15                default_names => 1,);
     16 $sel->open_ok($url);
     17 $sel->title_like(qr/Perl.Magazin/);

Bevor der Test durchgeführt werden kann, muss noch der Selenium-Server gestartet werden, der dann als Proxy dient. Der Server nimmt die Anfragen aus dem Skript entgegen und leitet diese dann weiter.