BLOG

Simple Test mit Drupal

Tests sind eine wichtige Sache für moderne Softwareentwicklung. Mit Drupal haben wir die Möglichkeit Simple Test zu verwenden. In Drupal 7 ist es zum Standard geworden und der ganze Kern von Drupal wird damit getestet und seit neuestem auch einige Module. Das ganze wird übrigens verwaltet von Boombatower, dessen Blog immer nützliche Informationen rund ums Testen liefert.
Simple Test ist eine Mischung aus Web Crawler und DOM Parser mit einigen nützlichen Funktionen. Wir benutzen eine Objektsammlung mit einem komplett leeren Drupal, konfigurieren uns dieses so hin, wie wir es brauchen, laden Seiten und schauen dann, ob dabei Daten übertragen werden wie wir es erwarten.
Wir haben hier einen deterministischen Ansatz. Auf die gleiche Abfolge logischer Schritte kommen immer die gleichen Antworten. Wenn nicht, läuft etwas falsch.

Wie erstelle ich also einen Test, was kann ich testen und wo liegen die Grenzen?

Voraussetzungen

Um in Drupal 6 zu testen muss zuerst der Core Patch aus dem SimpleTest Modul angebracht werden. Es gibt aber auch das Bestreben, den Core von vornherein zu patchen und uns das zu ersparen. Ich rechne damit, dass dies relativ schnell kommen wird. In Drupal 7 ist bereits alles vorhanden. Danach einfach das Modul aktivieren und loslegen.

Drupal 7 ist aber leider auch mit Vorsicht zu genießen. Es ist einfach unglaublich langsam in den Tests und man muss schon Tricks anwenden, wie MyIsam und keine InnoDB Tabellen in MySQL benutzen, damit Tests, die Sekunden dauern sollten nicht Minuten brauchen.

Ansonsten brauchen wir CURL für PHP und ein PHP ab 5.1 wegen des DOM Parsers.

Dateien

.test Dateien gehören in das /tests Verzeichnis unserer Module. hook_test wie noch auf lullabot.com beschrieben ist nicht mehr von Nöten, die Dateien werden automatisch entdeckt. In diese Datei schreiben wir die Klasse unseres Tests. Soweit ich weiß, kann ich beliebig viele Dateien anlegen um meine Tests zu strukturieren.

Die Beispiele sind aus dem Simpletest Tutorial "Mymodule" von drupal.org

DrupalWebTestCase

Unsere Test Klasse ist immer eine Ableitung von DrupalWebTestCase. Bitte seht euch unbedingt mal in der Datei drupal_web_test_case.php die Klasse an. In ihr stehen alle wichtigen Dinge zum eigentlichen Test. Besonders die setUp() Zeile 1068 Methode ist interessant.

Ihr findet hier auch alles sogenannten Assertions und Hilfsfunktionen, die ab und an mal etwas verspätet dokumentiert werden. Von daher lohnt sich immer ein Blick auf das Objekt, wenn es ein Update gibt.

<?php
class MymoduleTestCase extends DrupalWebTestCase {
?>

getInfo()

Damit SimpleTest etwas mit Eurer Klasse anfangen kann, müsst Ihr diese statische Methode implementieren, die dem SimpleTest Modul Informationen zu unserem Test bereitstellt. Die Daten aus dem Array sind wohl selbsterklärend.

<?php
 
public static function getInfo() {
    return array(
     
'name' => 'mymodule',
     
'description' => 'Ensure that the mymodule content type provided functions properly.',
     
'group' => 'Mymodule Demonstration Tests',
    );
  }
?>

setUp()

Der eigentliche Einstieg in den Test ist die setUp() Methode. In der Basis dieser Methode installiert unser Drupal, von dem wir aus testen, ein komplett leeres Drupal in englisch mit einem neuen, zufälligen Tabellen-Präfix. Die Tests arbeiten also in einer Sandbox, in der auch alle Module neu installiert werden müssen, und die keine Daten oder Dateien enthält. Wir machen also eine Ableitung der setUp() Methode und rufen die ursprüngliche Funktion auf. An diese können wir alle zu installierenden Module angeben. Für unsere Tests müssen wir also auch unser Modul und alle Abhängigkeiten aktivieren. Wir können auch davon ausgehen, dass Drupal standardmäßig installiert wird, also dass das Taxonomy Modul aktiviert ist, aber nicht das Color Modul.

In der setUp() Methode sollten wir auch alles aktivieren, was wir während des Lebens der Klasse brauchen. Im MyModule-Test wird zB. ein Nutzer erstellt, der gewisse Rechte hat und dieser wird auch gleich angemeldet.

<?php
 
public function setUp() {
   
parent::setUp('mymodule');  // Enable any modules required for the test
    // Create and log in our user
   
$privileged_user = $this->drupalCreateUser(array('create mymodule', 'edit own mymodule'));
   
$this->drupalLogin($privileged_user);
  }
?>

testMymoduleCreate()

Dies ist ein richtiger Test. Unser Drupal ist installiert und erwartet Anfragen. Wir füllen den SimpleTest-Browser, in dem wir die Hilfsfunktionen drupalPost oder drupalGet verwenden. In dem Beispiel wird mit drupalPost das Array $edit über eine vollständige HTTP POST Anfrage übergeben und dann geprüft, ob die Rückgabe den gewünschten Text enthält.

<?php
 
// Create a mymodule node using the node form
 
public function testMymoduleCreate() {
   
$edit = array(
     
'title' => $this->randomName(32),
     
'body' => $this->randomName(64),
    );
   
$this->drupalPost('node/add/mymodule', $edit, t('Save'));
   
$this->assertText(t('mymodule page @title has been created.', array('@title' => $edit['title'])));

   
// For debugging we can output the page so it can be opened with a browser
    // Remove this line when the test has been debugged
   
$this->outputScreenContents('After page creation', 'testMymoduleCreate');
  }
?>

Assertions / Annahmen

Die Assertions - also Annahmen - sind Methoden aus der DrupalWebTestCase Klasse, die dazu dienen - wie der Name schon sagt - gewisse Dinge anzunehmen. Ich hatte am Anfang Schwierigkeiten zu begreifen wie ich hier vorgehen soll. Ich wollte immer if() und exit() einbauen und meine Tests so strukturieren. Aber man sollte sich auf die Assertions verlassen und so einen Test aufbauen. Wenn ich also auf eine Variable prüfen will, dann sollte ich zB. $this->assertTrue oder zB. $this->assertEqual benutzen und kein if(). Damit haben unsere Tests eine klare und einfache Struktur. Sie fallen dann meist zusammen wie ein Kartenhaus, wenn einmal etwas scheitert, aber vielleicht kann man ja am weiteren Ablauf auch mehr sehen. Der Test läuft also immer durch und das sollte uns nicht stören.

Wir haben drei Gruppen an Assertions zur Verfügung.

Vergleiche

Wir können Variablen überprüfen, ob sie TRUE oder FALSE sind, gleich oder ungleich und solche Dinge mit diesen Assertions.

<?php
$this
->assertTrue($result, $message = FALSE, $group = 'Other')
?>

$result ist die Variable, die wir prüfen. $message ist das, was rot oder grün als Resultat angezeigt. $group ist die Gruppe der Assertions und wird meines Wissens kaum verwendet.

Man sollte das so verwenden, dass man immer einen sinnvollen Text eingibt, der Sinn ergibt. Bei einem != Test, bekommt man bei grün also am besten einen Text wie "A ist nicht gleich B", was ja eigentlich nach einem Fehler klingt, aber einfach stimmt für die Assertion.

Inhalt überprüfen

Wenn wir mit $this->drupalGet() oder $this->drupalPost() eine Seite laden, stellt uns der DrupalWebTestCase das Ergebnis der cUrl Anfrage als SimpleXML Objekt zur Verfügung. Mit den Assertions wie $this->assertText() fragen wir das DOM ab. Assert Text zum Beispiel enfernt alle HTML Tags und sucht in allem was übrig bleibt. $this->assertRaw sucht im vollständigen Ergebnis inklusive des HTML.

Formularelemente

Seit kurzem gibt es auch Assertions um Formularelemente zu überprüfen. Dies ist sehr praktisch und verkürzt den Aufwand zu prüfen, was ein Feld enthält oder ob eine Checkbox abgehakt ist. Das ganze funktioniert über eine XPath Abfrage. Wir können auch nach einem Feld über assertFieldByXPath() abfragen oder mit $this->xpath() selbst das DOM durchsuchen. Wichtig ist, dass das prüfen auf mehrfache Auswahl in Feldgruppen nicht funktioniert. Ich hoffe, dass das bald gepatcht wird.

API Funktionen

SimpleTest verfügt über eine ganze Menge an nützlichen Hilfsfunktionen. Ich möchte hier nicht die Dokumentation von drupal.org wiederholen und hier lieber Tipps zur Verwendung schreiben.

POST / GET

Die wichtigsten sind $this->drupalGet() und $this->drupalPost(). $this->drupalGet() lädt über HTTP GET eine Seite nach, die wir prüfen können. $this->drupalPost() nutzt HTTP POST und sendet in einem Array angegebene Variablen mit. Mit dieser Methode erledigen wir den Löwenanteil unserer Tests. Für die meisten Dinge in Drupal muss man Formulare ausfüllen und das tun wir hiermit. Die Keys im Array sind die Namen der zu füllenden Felder. Aus diesen erstellt die Methode POST Header.
Zu beachten ist, dass man Feldgruppen durch die Verwendung eines Arrays füllen kann. Man kann das Ergebnis zwar nur von Hand mit xpath prüfen, aber die API funktioniert für die Eingabe sauber.
Und die Feldnamen findet man am Besten mit Firebug. Das ist eine recht anstrengende Arbeit erstmal alle Feldnamen zu suchen, aber was soll man machen.

Beispiel von drupal.org

<?php
$name
= $this->randomName();
$mail = "$name@example.com";
$edit = array(
 
'name' => $name,
 
'mail' => $mail,
 
'status' => FALSE, // checkboxes must be set with TRUE/FALSE rather than 1/0
);
$this->drupalPost('user/register', $edit, 'Create new account');
?>

randomString / randomName

Diese Funktionen erstellen zufällige Zeichenketten. randomName() nur aus den Standard ASCII Zeichen. Man sollte diese Funktionen für alle Inhalte benutzen, die man prüfen will. Also keine statischen Zeichenketten setzen sondern über diese Funktionen immer zufällige benutzen. Damit erhöhen wir die Zuverlässigkeit unserer Tests, da wir hier das Einschleichen von Fehlern vermeiden.

drupalCreateUser

Diese Methode legt uns einen User an. Zu beachten ist, dass der User eine eigene Rolle bekommt, die über die übergebenen Rechte verfügt. Es fühlt sich komisch an, dass jeder User eine eigene Rolle hat, aber das ist im Allgemeinen unerheblich für den Test.

Emails testen

In Drupal kann man mit der Funktion drupal_mail_wrapper() eine eigene Versendemethode für Emails implementieren. Diese wird dann in der eigentlichen Versende Funktion drupal_mail_send() aufgerufen, wenn einige globale Variablen gesetzt wurden. SimpleTest tut dies und gibt uns die Möglichkeit gesendete Emails abzufangen und zu testen.

<?php
function drupal_mail_wrapper(array $message) {
 
$captured_emails = variable_get('drupal_test_email_collector', array());
 
$captured_emails[] = $message;
 
variable_set('drupal_test_email_collector', $captured_emails);
  return
TRUE;
}
?>

Die Daten werden in der Systemvariable "drupal_test_email_collector" zwischengespeichert, welche wir in unserem Test laden und mit Assertions prüfen können.
Beispiel aus dem ecard Modul-Test.
<?php
$mails
= variable_get('drupal_test_email_collector', array());

   
$this->assertEqual(count($mails), 2, '2 emails found.');

   
// Check original email.
   
$this->assertEqual($mails[0]['id'], 'ecard_ecard-mail', 'Correct email id found.');
   
$this->assertEqual($mails[0]['from'], $from_email, 'From address is correctly set.');
   
$this->assertEqual($mails[0]['to'], $to_email, 'To address is correctly set.');
?>

Falls euer Modul selbst drupal_mail_wrapper() nutzt, kann man gut im phpmailer Modul sehen, wie man verhindert, das in einem Test PHP abbricht, da die Funktion sonst doppelt deklariert wäre.

<?php
/**
* Determine if PHPMailer is used to deliver e-mails.
*/
function phpmailer_enabled() {
  return
strpos(variable_get('smtp_library', ''), 'phpmailer');
}

if (
phpmailer_enabled() && !function_exists('drupal_mail_wrapper')) {
 
/**
   * Implementation of drupal_mail_wrapper().
   */
 
function drupal_mail_wrapper($message) {
   
module_load_include('inc', 'phpmailer', 'includes/phpmailer.drupal');
    return
phpmailer_send($message);
  }
}
?>

Hooks testen mit Mock-Modulen

Wenn ihr einen Hook testen wollt, geht das am besten mit einem Mini-Modul, also einer Attrappe (eng. Mock). Der Trick hier ist, das man in der .info Datei des Moduls einfach eintragen kann, das es unsichtbar ist:

; $Id$
name = "XML-RPC Test"
description = "Support module for XML-RPC tests according to the validator1 specification."
package = Testing
version = VERSION
core = 7.x
files[] = xmlrpc_test.module

hidden = TRUE

Dann kann man einfach im setup() des Tests sein Modul aktivieren und nutzen.

Unit Tests

Mit dem aktuellen Backport von SimpleTest aus Drupal 7 kam auch eine Unit Test Funktionalität mit. Der Unit Test läuft im Grunde genauso wie andere Tests. Die Klasse die hier verwendet wird heißt "DrupalUnitTestCase". Der große Unterschied ist aber, das keine Datenbank zur Verfügung steht und auch keine Funktionen auf Dateien zu zu greifen. Wir testen also völlig isoliert eine Funktion, nach oben beschriebenem Muster.

Tests Debuggen

Richtig nervig ist es, wenn der Test nicht das tut, was wir wollen. Zuerst sollte man in unter 'admin/config/development/testing/settings' unbedingt 'Provide verbose information when running tests' anstellen. Dadurch wird zu jedem drupalPost und drupalGet das Ergebnis abgespeichert und man kann sich die Seite zusammen mit den übergebenen Variablen anschauen. Das ist Gold wert. Ich vergesse gern mal ein paar Rechte für den User und so sieht man sofort, wenn man nicht angemeldet ist oder Rechte fehlen. Das war zu Beginn auch anders und nun geht es über eine einfache Einstellung.
Ansonsten benutze ich gern mal eine Assertion zum Debuging, um einfach mal einen String auszugeben.

Nützlich ist auch die Funktion debug() aus dem common.inc. Diese schreibt uns direkt eine Ausgabe in den Test, praktischer weise auch gleich durch print_r().

<?php
/**
* Debug function used for outputting debug information.
*
* The debug information is passed on to trigger_error() after being converted
* to a string using _drupal_debug_message().
*
* @param $data
*   Data to be output.
* @param $label
*   Label to prefix the data.
* @param $print_r
*   Flag to switch between print_r() and var_export() for data conversion to
*   string. Set $print_r to TRUE when dealing with a recursive data structure
*   as var_export() will generate an error.
*/
function debug($data, $label = NULL, $print_r = FALSE) {
 
// Print $data contents to string.
 
$string = $print_r ? print_r($data, TRUE) : var_export($data, TRUE);
 
trigger_error(trim($label ? "$label: $string" : $string));
}
?>

OOP Tricks

Für PHP Entwickler meist ungewohnt sind ein paar nützliche OOP-Dinge um seine Tests zu organisieren. Es empfiehlt sich, eine eigene Variante von DrupalWebTestCase anzulegen und dann die eigentlichen Tests aus dieser zu vererben. Wenn ich zB. ein paar Vokabular für meine Tests brauche, lege ich mir eine private Methode an, um diese anzulegen und kann in jedem Derivat der Klasse das dann einfach verwenden, ohne die Methode jedes mal neu zu implementieren. Man kann ja auch die Basismethode aufrufen und seine eigene Variante erstellen.

Schaut einfach mal in den System.test rein. Hier wird ein ModuleTestCase erstellt und dann vielfach verwendet. Diese Klasse enthält eine Variable "$admin_user" durch die der im setup() angelegte User für alle Tests bereitgestellt wird.

Wichtig ist auch, dass die vielen verschiedenen Klassen dafür da sind, selbst die setup() Methode aufzurufen und damit in einer eigenen Sandbox zu laufen. Dies kann zum Beispiel erforderlich sein, wenn man statische Variablen in Funktionen hat, die nur pro Aufruf neu gesetzt werden. Das Problem hatte ich mit dem NAT Modul.

Einfach typisch OOP wie man es braucht. Nutzt es und ihr spart euch viel Mühe.

SimpleTest Clone

Das Modul SimpleTest Clone ist eine Ableitung des DrupalWebTestCase mit einigen nützlichen Funktionen. Es kann einfach einmal von Nutzen sein, dass eine bestehende Installation getestet wird. Dafür müssen die Daten in die Sandbox migriert werden und das ist grundsätzlich das, was dieses Modul leistet. Ich empfehle deutlich, seine Tests nicht an bestehenden Daten auszurichten. Ein Test muss für sich allein funktionieren. Aber es kann helfen doch einmal ein vorhandenes System zu klonen.

Selenium

Selenium ist im Prinzip ein Macro Generator für Firefox. Wir klicken uns durch unsere Website und zeichnen das auf. Mit SimpleTest kann man schlecht einen Geschäftsprozess testen. Im Endeffekt enden wir also dabei immer wieder die Formulare auszufüllen und zu schauen, ob man durch kommt. Der Trick an Selenium ist, das man dies einfach einmal aufzeichnet und dann immer wieder automatisiert abspulen kann. Wichtig ist, das die Maschine nichts vergisst oder schlampig sein kann. Wenn der Test gut aufgebaut wird, ist es eine große Hilfe.
Selenium kann aber noch viel mehr. Die Tests können ganz ähnlich wie in SimpleTest angepasst werden und man erreicht durch zufällige Eingaben höhere Sicherheit. Und mit verschiedenen Erweiterungen kann man seine Tests auch auf verschiedenen Browsern laufen lassen oder sogar auf einem ganzen Server-Grid. Das habe ich allerdings nie ausprobiert.
Für uns stellt Selenium eine sinnvolle Ergänzung zu SimpleTest da, um die wirkliche Funktionalität der Seite zu gewährleisten.

Tests für den Core

Wenn Ihr irgendwas für Drupal 7 patcht, geht davon aus, dass die erste Bitte sein wird, dafür Tests zu erstellen. Das nervt, ist aber absolut sinnvoll. Oft ist aber die Frage "Wie und wo?". Mein Tipp ist "Durchhalten!". Man lernt eine Menge dabei und es ist wirklich gut für die Community. Und lest die Richtlinien. Interessant ist, dass die Includes auch Tests haben und diese im simpletest-Modul enthalten sind.
Für mich war oft problematisch den richtigen Platz für den Test zu erkennen. Man sollte sich gut überlegen was man macht und rumfragen. Das System-Modul ist ein guter Platz für allgemeine Systemtests.

Was man nicht testen kann

Man wird wohl nie automatisch testen können, wie etwas aussieht. Die Darstellung könnte man wohl soweit überprüfen, ob ein CSS Style auch wirklich Teile des HTML beeinflusst, so wie Firebug das macht, aber wir haben keine Browser-Engine zur Verfügung. Vielleicht könnte man so etwas als Firebug-Plugin realisieren, aber das ist wohl recht abgefahren.

Themes haben meines Wissens keine Tests. Es gibt dafür eine Issue, aber ich habe leider vergessen wo.

Fazit: Es lohnt sich!

Das Test schreiben mag anstrengend sein und nervt. Ganz ähnlich wie das Dokumentieren. Aber am Ende spart man einfach nur Zeit. Wenn wir irgendwas ändern, müssen wir vielleicht sogar den Test umschreiben, aber wir werden ohne den Test nie so gut eine deterministische Grundlage in unsere Arbeit bekommen. Und wenn wir es öfter machen, dann werden wir auch schneller in der ganzen Sache und können unsere Arbeitsmethodik auf ein neues Niveau heben. Und man hat immer ein Schild gegen Aussagen wie "Das hat nie funktioniert" oder "Warum haben Sie denn nun alles kaputt gemacht."

Ich hoffe den einen oder anderen in Essen in meiner SimpleTest Session zu sehen!

Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
Simple Test mit Drupal
AnhangGröße
comm-press-simpletest.pdf1.65 MB

Kommentare

hallo, danke für das nette

hallo,

danke für das nette tutorial! die OOP-tricks verwende ich auch.

vielleicht hast du einen tipp für folgendes problem? beim testen meiner multilingual-site bekomme ich immer wieder das problem, dass bei den links der language-code immer doppelt gesetzt wird.
http://drupal.org/node/94...

lg jo/sef

Pingback

[...] my Simpletest Session in Essen , there were a few discussions about Selenium. With Selenium IDE you can click Firefox Tests [...]

Pingback

[...] meiner SimpleTest Session in Essen gab es ja einige Gespräche über Selenium. Mit der Selenium IDE kann man sich mit Firefox Tests [...]

Präsentationfertig

Nun mit Präsentation für DC Essen in Bild und PDF

Debug

Debug und Mail neu dazu! Text habe ich auch nochmal überarbeitet und Ralf hat auch nochmal drübergeschaut :)

Wie wäre es noch mit der

Wie wäre es noch mit der Funktion debug()

Man könnte vielleicht

Man könnte vielleicht http://seleniumhq.org/ für solche Dinge verwenden.

In einer Präsentation auf der DrupalCon-Paris wurde beschrieben, dass die Seite sogar automatisierte Tests für das Design macht, aber ich kann mich nicht mehr erinnnern wie.

Kommentar hinzufügen

Der Inhalt dieses Feldes wird nicht öffentlich zugänglich angezeigt.
Mit dem Absenden dieses Formulars, akzeptieren Sie die Datenschutzrichtlinie von Mollom.