Zombie mit Servo (Linux-Magazin, Oktober 2006)

Komplexe Webapplikationen zu testen, verlangt nach sündhaft teuren proprietären Tools wie TestDirector oder Silk Performer. Das neue Selenium gibt's umsonst, es kann Firefox, IE, Opera, Konqueror und Safari fernsteuern, und es lässt sich mit Perl programmieren.

Ob eine Webapplikation nach einer kleinen Codeänderung noch funktioniert, lässt sich nur dann mit Sicherheit sagen, wenn man alle angebotenen Funktionen mit einem Browser durchspielt. Jede Seite muss aufgeschlagen, jeder Button muss gedrückt und jedes Feld ausgefüllt werden. Und das nicht nur einmal, denn auch alle Erfolgs- und Fehlerszenarios wollen überprüft werden.

Werden solche Regressionstests wieder und wieder von Menschenhand ausgeführt, ist dies nicht nur mit enormen Kosten verbunden, sondern auch die QA-Abteilung ermüdet, Testschlampereien und -fehler häufen sich, und die Stimmung des Personals sinkt rapide. Ein automatischer Test muss her.

Schwieriges Einfach

Doch das ist nicht so einfach. Zwar lassen sich einfache Webapplikation mit Screen-Scrapern wie dem CPAN-Modul WWW::Mechanize testen, doch steigt dieses Tool schnell aus, falls die Website JavaScript verwendet. Zwar mangelt es nicht an JavaScript- Implementierungen mit zugehörigen Perl-Schnittstellen, doch die Interaktion des JavaScript-Engines mit dem DOM (Document Object Model) des Browsers wurde bisher noch nicht zufriedenstellend gelöst. Sie ist auch ausgesprochen komplex, denn mit JavaScript ist es möglich, das HTML einer geladene Seite zu manipulieren, periodisch Aktionen auszulösen, neue Fenster zu öffnen oder gar neue Daten per Ajax vom Server zu holen und diese in die Seitendarstellung einfließen zu lassen. Und natürlich hat jeder Browser seine eigenen Macken und seine eigene Vorstellung von einer korrekten Implementierung des DOM.

Vor kurzem brachte die Firma ThoughtWorks das Opensource-Projekt Selenium auf openqa.org ins Rollen, dass das Problem auf verblüffend simple Weise löst. In den Webbrowser eingetrichterter JavaScript-Code macht es möglich, alle wichtigen Browser fernzusteuern und Tests zu automatisieren.

Abbildung 1 zeigt ein einfaches Webformular, das User-Kommentare entgegennimmt. Vor dem Kommentartext gibt der Benutzer seine Email-Adresse an, damit der Webmaster später eine Antwort zurückschicken kann. Damit das Email-Feld nicht irrtümlich leer bleibt oder Text enthält, der offensichtlich keine Email-Adresse ist, enthält die Seite JavaScript-Code, der die Eingabe prüft, wenn der ``Send''-Button gedrückt wird. Stellt JavaScript einen Fehler fest, wird der Kommentar nicht zum Server abgeschickt, sondern eine Warnung angezeigt.

Die Implementierung dieser Seite ist denkbar einfach, Abbildung 2 zeigt den erforderlichen HTML- und JavaScript-Code. Aber wie lässt sich diese Seite aber nun zuverlässig und reproduzierbar testen? Das Aufklappen des Dialogfensters mit der Warnung sollte genauso verifiziert werden wie die Erfolgsmeldung im Fall einer gültigen Emailadresse, wenn das hinter dem ACTION-URL /cgi/feedback.cgi hängende CGI-Skript ``Thanks for your feedback!'' zurückschreibt.

Abbildung 1: Ein einfaches Webformular, das eine JavaScript-Warnung auslöst, falls eine ungültige Email-Adresse eingegeben wurde.

Abbildung 2: HTML und JavaScript zur Überprüfung der Email in einem Feedback-Formular.

Listing emailcheck zeigt ein Testskript, das zunächst eine Verbindung zum Selenium-Server aufbaut. Dieser lässt auf den Befehl start() hin einen Firefox-Browser in einer isolierten Session hochfahren. open() veranlasst ihn anschließend dazu, die HTML-Seite von Abbildung 1 vom Webserver zu holen, die den JavaScript-Code zur Emailprüfung enthält.

Listing 1: emailcheck

    01 #!/usr/bin/perl -w
    02 use WWW::Selenium;
    03 use Log::Log4perl qw(:easy);
    04 Log::Log4perl->easy_init($DEBUG);
    05 
    06 my $url = "http://perlmeister.com";
    07 
    08 my $sel = WWW::Selenium->new(
    09   host => "localhost", 
    10   port => 4444, 
    11   browser => "*firefox " . 
    12     "$ENV{FIREFOX_HOME}/firefox-bin", 
    13   browser_url => $url,
    14 );
    15 
    16   DEBUG "Starting";
    17 $sel->start();
    18 
    19 $sel->open("$url/test/mail.html");
    20   DEBUG "Typing email";
    21 $sel->type("from", 'abcde');
    22   DEBUG "Clicking send";
    23 $sel->click("send");
    24 my $alert = $sel->get_alert();
    25   DEBUG "alert was '$alert'";
    26 
    27   DEBUG "Opening";
    28 $sel->open("$url/test/mail.html");
    29   DEBUG "Typing email";
    30 $sel->type("from", 'abcde@foo.com');
    31   DEBUG "Clicking send";
    32 $sel->click("send");
    33 $sel->wait_for_page_to_load(50000);
    34 my $body = $sel->get_body_text();
    35   DEBUG "Response was '$body'";
    36 
    37 $sel->stop();

Gültig nur mit Affe

Die Methode type() simuliert einen tippenden Benutzer und nimmt als Argumente den Namen des <INPUT>-Feldes eines HTML-Formulars und den zu tippenden Text entgegen. In diesem ersten Testfall füllt WWW::Selenium in Zeile 21 den String "abcde" in das Email-Feld mit dem name-Attribut "from" ein. Die anschließend aufgerufene Methode click() mit dem Namen des Submit-Buttons als Argument (in diesem Fall ``send'') simuliert einen Klick auf den Submit-Button.

Da der Adressenstring in diesem Fall kein @ und keinen Punkt enthält, handelt es sich bei der Eingabe eindeutig um keine gültige Emailadresse und der JavaScript-Code der Webseite lässt den Warnungsdialog hochfahren. get_alert() schnappt sich diesen in Zeile 24 und liefert den Fehlertext. Zu Testzwecken druckt emailcheck den gefundenen Textstring aus. Abbildung 3 zeigt die Debug-Ausgabe des Skripts.

Abbildung 3: Ausgabe des Testscripts emailcheck, das mit Selenium einen Firefox-Browser fernsteuert.

Im zweiten Testfall ab Zeile 27 gibt der Simulator den String "abcde@foo.com" in das Email-Feld ein, also eine syntaktisch richtige Email-Adresse. Nach dem click() wartet der Simulator mit wait_for_page_to_load() bis zu 50 Sekunden (der angegebene Wert von 50.000 ist in Millisekunden), bis sich die auf den Klick folgende Seite aufgebaut hat. Den Inhalt der fertig geladenen Seite liest dann get_body_text() aus und druckt ihn ebenfalls zu Testzwecken aus. stop() schließt am Ende den vorher mit start() geöffneten Browser wieder.

Tests en Masse

Da Selenium-Skripts hauptsächlich für Regressionstests dienen, steht mit Test::WWW::Selenium ein CPAN-Modul bereit, das den Selenium-Client auf einfache Weise mit dem in Perl üblichen und in [4] schon einmal besprochenen Test Anything Protocol (TAP) erweitert. Skripts, die Ausgaben in diesem Format erzeugen, lassen sich einfach mit Modulen wie Test::Harness zu stetig wachsenden Regressions-Testsuiten zusammenfassen, die einfach aufzurufen sind und übersichtlich formatierte Ergebnisse ausspucken.

Test::WWW::Selenium erweitert hierzu die Klasse WWW::Selenium mit einigen Befehlen, die gleichzeitig Selenium-Befehle abfeuern und prüfen, ob diese auch die erwartete Wirkung zeigten. Statt mit open() eine Webseite zu laden, wird nun open_ok() aufgerufen. Im Erfolgsfall wird denn entsprechend dem TAP-Protokoll ok 1 ausgegeben, tritt ein Fehler auf, kommt not ok 1 zum Vorschein.

Aus den WWW::Selenium-Methoden zum Einholen von Seitendetails wie get_body_text() oder get_title() wird das get_ gestrichen und der Name einer Prüfmethode aus dem Fundus von Test::More angehängt. So prüft title_is($soll) zum Beispiel, ob der von get_title() gelieferte Seitentitel exakt dem in $soll abgelegten String entspricht. Und das von get_body_text() abgeleitete body_text_like($regex) druckt den Erfolgstext, falls der in $regex gespeicherte reguläre Ausdruck auf den Seitentext passt.

Der Zombie googelt

Das Testskript gtest fährt Firefox hoch, geht zu Google, tippt den String ``schilli'' in das Suchfeld ein und drückt den Button Google Search. Die Testfälle prüfen, ob die Suche erfolgreich war und ob in der Trefferliste irgendwo der String "perlmeister" auftaucht. Damit die Methode click() weiss, welcher Button zu drücken ist, nimmt sie normalerweise den Wert des name-Attributs des Buttons entgegen. Stattdessen kann man das Klickziel aber auch über einen sogenannten Element Locator bestimmen. Mit der XPath-Syntax

    //input[@value="Google Search"]

steuert click() so das <INPUT>-Element eines Formulars an, dessen value-Attribut den Wert "Google Search" enthält. Abbildung 4 zeigt die Ausgabe des Testskripts, das alle vier definierten Testfälle erfolgreich durchläuft.

Abbildung 4: Ausgabe des Testscripts gtest.

Abbildung 5: Der Browser geht ferngesteuert zu Google und leitet eine Suche ein.

Die Dokumentation von WWW::Selenium führt eine ausführliche Liste von Methoden auf, mit denen der Client den Selenium-Server und damit den Webbrowser fernsteuern kann. Selenium unterstützt die ganze Palette des Browser-Schnickschnacks, vom Öffnen mehrerer Popup-Windows über das Auslösen von JavaScript-Events bis zum Platzieren des Textcursors in Formelementen.

Listing 2: gtest

    01 #!/usr/bin/perl -w
    02 use Test::WWW::Selenium;
    03 use Test::More tests => 4;
    04 
    05 my $url = "http://www.google.com";
    06 
    07 my $sel = Test::WWW::Selenium->new(
    08   host => "localhost", 
    09   port => 4444, 
    10   browser => "*firefox " . 
    11     "$ENV{FIREFOX_HOME}/firefox-bin", 
    12   browser_url => $url,
    13 );
    14 
    15 $sel->open_ok($url);
    16 
    17 $sel->type_ok("q", "schilli", 
    18               "Type query");
    19 
    20 $sel->click_ok(
    21         '//input[@value="Google Search"]', 
    22         "Clicking Search");
    23 $sel->wait_for_page_to_load(5000);
    24 
    25 $sel->body_text_like(qr/perlmeister/, 
    26         "perlmeister found");

Öffnen'se mal die Haube!

In Abbildung 5 ist der während des Testlaufs ferngesteuerte Firefox zu sehen. Die obere Hälfte zeigt die von Selenium eingespeisten Bedienelemente, unt ist die gerade gestestete Website zu sehen. Es fällt auf, dass der Browser-Zombie völlig unkonfiguriert ist. Der personalisierbare Toolbar ist leer und auch das Suchfenster ist auf Google voreingestellt, während mein Firefox natürlich die Suchmaschine meines Arbeitgebers nutzt. Grund für das andere Erscheinungsbild ist, dass die Selenium-Fernsteuerung für ihre Zwecke ein eigenes Firefox-Profile anlegt.

Ein prüfender Blick auf das Location-Feld des Browsers zeigt auch, dass hier einiges nicht mit rechten Dingen zugeht. Der dargestellte URL

    http://www.google.com/selenium-server/...

jubelt Google einen Selenium-Server unter, der auf der Website es Suchgiganten so garantiert nicht installiert ist. Das Diagramm in Abbildung 6 erklärt das Phänomen. Die oben vorgestellen Testskripts kommunizieren nicht direkt mit dem Browser, sondern über die Selenium Remote Control als Mittelsmann.

Der Selenium-Server erfüllt dabei zwei völlig von einander getrennte Funktionen. Fordert ein Testskript eine Internetseite an, leitet die Selenium-Fernsteuerung den Auftrag an den Browser weiter.

Dieser wurde aber vom Selenium-Server beim Hochfahren so eingestellt, dass er nicht direkt mit dem Internet kommuniziert, sondern nur durch einen Proxy. Dieser Proxy ist freilich niemand anders als der Selenium- Server, der so nicht nur genau mitbekommt, was der Browser mit dem Internet austauscht, sondern auch den Datenverkehr zwischen beiden Parteien beliebig manipulieren kann.

Bittet so die Selenium-Fernsteuerung den Browser, einen URL anzufordern, hängt sie an den URL einige Kontrollparameter und eine Session-ID an. Der Browser holt die angegebene Webseite allerdings nicht vom Netz, sondern bittet den eingestellten Proxy darum. So erhält der Selenium-Server wieder den Auftrag für den URL, filtert die für ihn bestimmten Geheimnachrichten heraus, kontaktiert den angegebenen Webserver, und holt die Antwort ab. Diese liefert er aber nicht direkt an seinen Auftraggeber, den Browser, zurück, sondern pflanzt vorher noch einen Wust an JavaScript-Code hinein, der die definierte Fernsteuerungsaktion (z.B. das Tippen von Text in ein Textfeld) im Browser auslösen wird.

Es sieht so aus, als könne der Selenium-Server den Browser fernsteuern, doch in Wirklichkeit fragt lediglich das dem Browser dynamisch eingetrichterte JavaScript in periodischen Abständen beim Selenium-Server nach, was denn als nächstes geschehen soll. Mit diesem Trick kann die Fernsteuerung den Browser ähnlich manipulieren wie wenn ein Firefox-Plugin installiert wäre. Die Fernsteuerung hat allerdings den Vorteil, dass sie Browser verschiedenster Hersteller nach ihrer Pfeife tanzen lässt.

Abbildung 6: Die mit Test::WWW::Selenium geschriebene Testsuite fernsteuert einen Browser über den Selenium-Server.

(Anmerkung: Im Diagramm bitte s/WWW::Test::Selenium/Test::WWW::Selenium ersetzen)

Die Selenium-Fernsteuerung ist in Java geschrieben und unter einer Apache-2.0-Lizenz verfügbar. Release 0.8.1 ist auf [2] als Zipdatei verfügbar, sie enthält sowohl die Java-Sourcen als auch die fertig compilierte .jar-Datei. Der janusköpfige Server wird mit

    LD_LIBRARY_PATH=/path/to/firefox-1.5 java -jar selenium-server.jar

gestartet. Wichtig ist, dass sich in /path/to/firefox-1.5 die dynamischen Libraries der Firefox-Installation (z.B. libmozjs.so) befinden. Ist der Browser standardgemäß installiert, darf die Manipulation des LD_LIBRARY_PATH wegfallen. Ähnliches gilt für den Konstruktor new der Klasse WWW::Selenium. Statt dem langwierigen Installationspfad darf der Parameter browser dann einfach den Wert "*firefox" erhalten. Der vollständige Pfad zum Firefox-Binary, der mit Hilfe der Umgebungsvariablen FIREFOX_HOME in den Listings angegeben wurde, kann dann wegfallen.

Der Server schnappt sich den Port 4444 und lässt dort mit sich reden. Der Konstruktor von WWW::Selenium verbindet sich in den vorgestellten Testskripts wegen des auf den Wert 4444 gesetzten Parameters port mit dem Server.

Das Prinzip ``Same Origin''

Eine kleine Einschränkung ergibt sich allerdings aus den Sicherheitsbeschränkungen, denen JavaScript unterliegt. Wegen des ``same origin''-Prinzips darf der JavaScript-Code von Domain A nicht den Inhalt einer Webseite auf Domain B manipulieren. Aus diesem Grund nimmt der Konstruktor der Klasse WWW::Selenium mit dem Parameter browser_url den Basis-URL der geprüften Website entgegen. Nachfolgende Tests können dann nur auf der angegebenen Domain erfolgen. Testscripts, die das Zusammenspiel mehrere Domains testen, sind derzeit noch nicht möglich, allerdings wird im nächsten Release von Selenium eine Möglichkeit geboten, diesen Basis-URL dynamisch zu verändern.

Eingestöpselt

Selenium bietet auch noch einen Plugin für den Firefox an, der Selenium IDE heißt und nach dem Aktivieren (Abbildung 7) ein Dialogfenster nach Abbildung 8 aufpoppen lässt. Dieses springt sofort in den Aufnahmemodus und protokolliert im Browser ausgeführte Aktionen in der von Selenium genutzten Sprache Selenese. Die so mitgeschnittenen Schritte reproduziert die Selenium IDE mit Leichtigkeit und erlaubt so die Definition von Testsuiten. Und die so protokollierten Daten lassen sich auch extrahieren, um daraus umfangreiche Regressionstests in Perl zu schreiben.

Abbildung 7: Der neu installierte Firefox-Plugin für die Silenium-IDE wird gestartet

Abbildung 8: Das UI der Silenium-IDE

In Nachbars Garten

Ein mit Selenium ausgestattetes Perlscript kann ohne Probleme auch Browser auf anderen Rechnern oder sogar feindlichen Betriebssystemen fernsteuern. So lässt sich die Kompatibilität einer Webapplikation für eine Reihe von Browsern prüfen. Um zum Beispiel den Microsoft Internet Explorer auf Windows fernzusteuern, kopiert man die .jar-Datei der Selenium Remote Control auf ein Windows-System und startet den Server mit

   java -jar selenium-server.jar

Falls die Java JRE noch nicht auf dem Windows-Rechner installiert ist, kann man sie von sun.com abholen und problemlos installieren. Die IP des Windows-Rechners bekommt man mit ipconfig in der Kommando-Shell heraus und wenn diese dem Konstruktor von WWW::Selenium auf dem Steuerungsrechner mit

  host => "192.168.0.70",
  port => 4444,
  browser => "*iexplore",

überreicht wird, treibt dieser seine Testsuiten anstandslos durch den Internet-Explorer auf der gefügig gemachten Windows-Kiste. Ein netter Gag für nichtsahnende Windows-Nutzer! Die Test-Skripts können weiterhin auf dem ursprünglichen System laufen. Abbildung 8 zeigt die von Selenium veröffentlichte Liste der gegenwärtig unterstützten Browser auf verschiedenen Betriebssystemen. Da ist für jeden Geschmack etwas dabei und der in Java geschriebene Fernsteuerungsserver läuft ebenfalls auf allen von Java unterstützten Platformen.

Abbildung 9: Die von Selenium unterstützten Browser

Installation

Die Module WWW::Selenium und Test::WWW::Selenium können vom CPAN bezogen werden. Vor dem Start der Testskripts emailcheck und gtest ist die Umgebungsvariable FIREFOX_HOME auf das Verzeichnis einzustellen, in dem das Binary firefox-bin des Firefox-Browsers liegt. Falls Firefox standardgemäß installiert wurde, kann der Parameter browser auch einfach auf ``*firefox'' gesetzt werden. Wenn der Selenium-Server, wie oben beschrieben läuft, öffnen die Skripts den Firefox, lassen die Testfälle ablaufen und liefern die Skripts die gezeigten Ergebnisse. Und statt monotone Tests von Hand durchzuführen, darf die QA-Abteilung nun Perl lernen. Da knallen die Champagnerkorken.

Infos

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

[2]
Selenium-Homepage: http://www.openqa.org/selenium

[3]
Die Selenium-IDE als Firefox-Plugin: http://www.openqa.org/selenium-ide/download.action

[4]
Michael Schilli, ``Kontrolle ist besser'', Regressionstests mit Perl, http://www.linux-magazin.de/Artikel/ausgabe/2005/11/perl/perl.html

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.