Aaron Fischer Ingenieur, Vater, Heimwerker, Problemlöser

06 Dezember, 2008

Funktionale- und Integrationstests mit Selenium

Technologie

Webseiten testen macht kein Spaß. Vor allem dann nicht, wenn man ein zweiseitiges Formular schon zum 25sten Mal ausgefüllt hat und immer noch ein Bug drin ist. Da ich gerne Dinge so weit es geht automatisiere, habe ich mich vor etwas längerer Zeit (diesen Blog-Post wollte ich schon vor ca. einem Jahr erstellen :) nach Test-Frameworks für Webseiten umgesehen.

Es gibt unzählige, allen voran SimpleTest und PHPUnit. Allerdings haben all diese Frameworks ein Problem: Die Testfälle sind dermaßen aufwendig zu erstellen, dass es bei größeren und komplexeren Seiten einfach zu zeitaufwändig und langweilig ist. Ein Beispiel hierfür aus dem Tutorial von SimpleTest:

function testWeAreTopOfGoogle() {
    $this->get("http://google.com/");
    $this->setField("q", "simpletest");
    $this->click("I"m Feeling Lucky");
    $this->assertTitle("SimpleTest - Unit Testing for PHP");
}

Um diesen Vorgang zu beschleunigen, habe ich mich weiter auf die Suche gemacht und bin dann eines Tages auf Selenium gestoßen. Hiermit lassen sich die Tests viel einfacher erstellen.

Selenium besteht aus mehreren Komponenten. Zum einen hat man zum Erstellen der Tests ein Firefox-Plugin, mit dem Tests einfach aufgezeichnet werden können:

Selenium-Tests im Browser aufzeichnen

Man drückt auf Record und beginnt mit seinem Testcase. Asserts lassen sich über das Kontextmenü hinzufügen. Will man beispielsweise sicherstellen, dass auf der aktuellen Seite eine Wortkombination erschienen ist, markiert man diese und wählt im Kontextmenü assertTextPresent. Auch Formulareingaben, Seitensprünge usw. werden aufgezeichnet. Will man kompliziertere Testfälle aufsetzen, kann man die aufgenommenen Schritte nachträglich bearbeiten und erweitern. Reichen die Befehle von Selenium nicht aus, kann immer noch auf JavaScript ausgewichen werden.

Hat man seinen Testfall fertig, kann man diesen auf unterschiedliche Weise abspeichern. Zum einen kann man den Testfall als simple HTML-Tabelle abspeichern, das ist das Standardformat von Selenium. Eine weitere Möglichkeit ist das Konvertieren in einer der vielen Programmiersprachen (Java, PHP, Ruby, Python, ...), so dass man am Ende ein ähnliches Ergebnis wie beim SimpleTest hat, nur mit viel weniger Tipparbeit. Dieses Script kann dann einfach ausgeführt werden.

Ein weiteres Modul von Selenium ist die Umgebung, die die aufgenommenen Testfälle starten kann. Hier gibt es wiederum mehrere Möglichkeiten. Die Serverkomponente ist für den Hausgebrauch relativ unbedeutend, und spielt erst eine entscheidende Rolle, wenn wirklich bei jedem Code-commit alle Testfälle ausgeführt werden sollen. Die einfachere Variante ist der Test-Runner, der komplett in JavaScript geschrieben ist, und somit direkt im Browser läuft. Das hat den Vorteil, dass man nicht an einen bestimmten Browser gebunden ist und somit seine Testfälle auf jedem beliebigen Browser testen kann, der einigermaßen neues JavaScript unterstützt.

Der Browser ist die Testumgebung

Ich habe mir, um den Testvorgang weitgehend zu automatisieren, eine Subdomain angelegt, die - wenn ich sie aufrufe - automatisch alle Tests durchlaufen werden. So kann ich in regelmäßigen Abständen und bei größeren Code-Änderungen immer prüfen, ob noch alles tut so wie es soll.

SetUp und TearDown?

Ein Problem gibt es dennoch mit Selenium: Wenn man beispielsweise ein Formular auf ungültige Eingaben testet, werden diese Testdaten evtl. in die Datenbank geschrieben und bleiben auch nach Beendigung des Testlaufs bestehen. Dies ist natürlich nicht gewollt. Eine SetUp- und eine TearDown-Methode muss her. Leider gibt es so etwas in Selenium nicht, da das Framework logischerweise die eigene Datenbank nicht kennt und auch keinen Zugriff darauf hat.

Um dieses Problem zu umgehen, habe ich mir ein kleines PHP-Skript geschrieben, dass diese Funktionalität nachrüstet. Abgespeckt sieht es ungefähr so aus:

define("SECRET", "12345xyz");

if (isset($_GET["action"])) {
    $key = isset($_GET["secret"]) ? $_GET["secret"] : "";
    if ($key == SECRET) dispatch($_GET["action"], $_GET);
    else die("Wrong secret, sorry.");
}

function dispatch($action, $params) {
    $preparator = new SeleniumPreparator();
    if (method_exists($preparator, $action)) {
        PicoraActiveRecord::connect(CONNECTION_STRING);
        output($preparator->$action($params));
    } else die("No method $action found.");
}
class SeleniumPreparator {
    public function setup() {
        // Make temp-Entry visible
        $entry = $this->getTestentry();
        $entry->display = 1;
        $entry->allow_comments = 1;
        $entry->save();
        return true;
    }

    public function teardown() {
        $entry = $this->getTestentry();
        $entry->display = 0;
        $entry->allow_comments = 0;

        // Clear all comments and annotations
        $comments = Comment::findAllByField("Comment", "entry_id", $entry->id);
        foreach ($comments as $c) {
            $c->delete();
        }   
        $entry->annotations = "[]";
        $entry->save(); 
        return true;
    }

    public function activateAllComments() {
        $entryID = $this->getTestentry()->id;
        $comments = Comment::findAllByField("Comment", "entry_id", $entryID);
        foreach ($comments as $c) {
            $c->approved = 1;
            $c->save();
        }
        return true;
    }

    private function getTestentry() {
        return Entry::findByField("Entry", "url_name", "selenium-testentry");
    }
}

Aufgerufen wird das Skript mit test_setup.php?secret=12345xyz&action=setup, um beispielsweise die Setup-Methode aufzurufen, die den Testeintrag sichtbar macht. Diesen Aufruf kann man natürlich auch mit Selenium machen. Mit dieser Technik lassen sich dann auch administrative Aufgaben wie das Aktivieren von Kommentaren durchführen, ohne manuell einzugreifen.

Ich hoffe, dass durch Selenium mehr Web-Developer ihre Webseite testen, denn das hat das Web dringend nötig.