Unit-Tests sorgen für funktionierende und wartbare Software. Dieser Artikel zeigt PHP-Entwicklern, wie sie mit dem Tool PHP Unit den Einstieg in die testgetriebene Entwicklung finden.
Unit-Testing ist vor allem bei größeren Softwareprojekten eine essenzielle Technik, die im Repertoire des Entwicklers nicht mehr fehlen darf. Das Standardwerkzeug unter PHP ist das Kommandozeilen-Programm PHP Unit [1]. Daneben existieren ein paar alternative Werkzeuge, die aber mangels Verbreitung und wegen ihres geringen Funktionsumfangs derzeit nicht empfehlenswert sind.
Tests für PHP-Projekte
Dieser Artikel erläutert, wie PHP-Entwickler das Testen mit PHP Unit in ihre eigenen Projekte integrieren. Dazu dient ein kleines Codebeispiel des Autors, der am Horde-Framework [2] und dessen Anwendungen mitarbeitet. Das Softwareprojekt ging mit 13 Jahren Tradition, über einer Million Codezeilen und 0 Prozent Testabdeckung an die Einführung von Unit-Tests. Mittlerweile existieren über 3000 Tests, die immer wieder helfen Fehler frühzeitig zu entdecken. Auf den meisten Distributionen installiert der Admin PHP Unit, indem er den Paketmanager auffordert »phpunit« einzuspielen. Als distributionsunabhängigen Standardweg der Installation gibt das PHP-Unit-Projekt selbst die PEAR-basierte Installation vor. Die Installation besteht in diesem Fall aus folgenden Kommandos:
pear channel-discover pear.phpunit.de pear channel-discover components.ez.no pear channel-discover pear.symfony-project.com pear install phpunit/PHPUnit
Als Programmierbeispiel dient ein minimaler Client für Gravatar, einen Onlinedienst, der seinen Benutzern individuelle Avatar-Bildchen zum Einbinden in Foren, Blogs und Websites zur Verfügung stellt [3]. Dabei geht es – wie häufig bei der Serversprache PHP – um die Kommunikation mit externen Systemen.
Der Code erhält die Form eines Horde-Pakets mit dem Namen »Horde_Service_Gravatar« . Im Horde-Git-Repository [4] lässt sich das Beispiel in kleinen Schritten (Git-Tags »hgs_step0« bis »hgs_step15« ) nachverfolgen, was in diesem Artikel aus Platzgründen nicht in gleicher Ausführlichkeit möglich ist.
Außerdem gibt das Horde-Projekt Strukturen für die Organisation der Codedateien im Paket vor. Beispielsweise ist vorgeschrieben, den Bibliothekscode und die Tests getrennt in den Verzeichnissen »lib« und »test« abzulegen (siehe Abbildung 1). Das erleichtert sowohl PHP Unit als auch anderen Programmierern das Auffinden der Tests. Die von Horde vorgegebene Struktur erlaubt zudem Autoloading nach PSR-0 [5].
Das oben angesprochene »lib« -Verzeichnis muss sich für die folgenden Beispiele aber im Include-Pfad von PHP befinden. Aus dem Arbeitsverzeichnis »Service _Gravatar/test/Horde/Service/Gravatar« erreicht der Entwickler das am einfachsten, indem er die Option »-d include_path =”../../../../lib:/usr/share/php”« an den »phpunit« -Aufruf anhängt. »/usr/share/php« verweist dabei auf das Verzeichnis, das die Klassen von PHP Unit bereithält.
Testgetrieben
Das Programmieren beginnt mit einer leeren Definition der Zielklasse in »Service_Gravatar/lib/Horde/Service/Gravatar.php« :
class Horde_Service_Gravatar { }
Das von Grund auf neu geschriebene Beispiel erlaubt es, völlig “Test-driven” zu programmieren, also die Tests vor der eigentlichen Implementierung zu schreiben [6]. Das mag im Alltag nicht einfach erscheinen: Vielleicht ist der Code schon älter und lädt ohne nennenswerte Testabdeckung nicht gerade zur testgetriebenen Weiterentwicklung ein.
Manche Entwicklungsaufgaben eignen sich auch weniger für diese Methode als andere: So ist Code, der über HTTP, IMAP, SQL oder Ähnliches mit einem anderen System kommuniziert, deutlich schwieriger zu testen. Wer sich an die Empfehlungen der Unit-Test-Puristen hält, wird aber im Normalfall von besser wartbarem Code profitieren.
Die ersten Zeilen
Zurück zum Beispiel: Der Testcode folgt ähnlichen Strukturen wie der eigentliche Code. Unter »test« liegt die Verzeichnisstruktur »Service_Gravatar/test/Horde/Service/Gravatar« , und hier beginnt die Arbeit an der Datei »GravatarTest.php« :
require_once 'Horde/Service/Gravatar.php'; class Horde_Service_Gravatar_GravatarTest extends PHPUnit_Framework_TestCase { }
Die Klasse leitet sich von »PHPUnit_Framework_TestCase« ab und signalisiert PHP Unit damit, dass es sich hier um eine Testsuite handelt. Außerdem lädt die Datei die Definition von »Horde_Service_Gravatar« .
Das Kommando »phpunit GravatarTest.php« sollte keine grundlegenden Probleme hervorrufen, wird aber wie in Abbildung 2 mit dem Hinweis fehlschlagen, dass noch keinerlei Tests vorhanden sind. Der nächste Schritt erledigt das Mapping der E-Mail-Adressen auf die Gravatar-ID und generiert die Avatar- und Profil-URLs. Das zu implementierende Verfahren ist in der API-Definition des Anbieters [3] beschrieben.
Listing 1 zeigt den Beginn der Testsequenz, die zum Ziel führt. Die entsprechenden Commits im Git-Repository [4] demonstrieren schrittweise die testgetriebene Entwicklung. Auf jeden neu definierten, fehlschlagenden Test folgt die Implementierung des passenden Code für den erfolgreichen Durchlauf des Tests.
Listing 1
Die ersten Tests
01 public function testReturn() 02 { 03 $g = new Horde_Service_Gravatar(); 04 $this->assertType('string', $g->getId('test')); 05 } 06 07 public function testAddress() 08 { 09 $g = new Horde_Service_Gravatar(); 10 $this->assertEquals( 11 '0c17bf66e649070167701d2d3cd71711', 12 $g->getId('test@example.org') 13 ); 14 } 15 16 /** 17 * @dataProvider provideAddresses 18 */ 19 public function testAddresses($mail, $id) 20 { 21 $g = new Horde_Service_Gravatar(); 22 $this->assertEquals($id, $g->getId($mail)); 23 } 24 25 public function provideAddresses() 26 { 27 return array( 28 array('test@example.org', '0c17bf66e649070167701d2d3cd71711'), 29 array('x@example.org', 'ae46d8cbbb834a85db7287f8342d0c42'), 30 array('test@example.com', '55502f40dc8b7c769880b10874abc9d0'), 31 ); 32 }
Das lässt sich durch Betrachten der Tags »hgs_step0« bis »hgs_step15« nachvollziehen. Die Definition des ersten Tests durch »public function testReturn()« zeigt: Der Programmierer teilt PHP Unit durch das »test« -Präfix des Methodennamens mit, dass es sich um einen Test handelt. Alle Testmethoden einer Suite müssen mit »public« als öffentlich markiert sein, sonst kann PHP Unit sie nicht ausführen. Im Normalfall nehmen Testmethoden keine Argumente entgegen.
Der Test selbst ist denkbar einfach aufgebaut: Die erste Zeile erzeugt mit »$g = new Horde_Service_Gravatar();« einen Gravatar-Client. Das ist ganz normaler PHP-Code, der die Testbasis aufbaut und nichts mit PHP Unit zu tun hat.
Behauptungen
Die Magie von PHP Unit kommt in der vierten Zeile zum Tragen. Sie setzt mit »assertType()« eine Assertion (Behauptung) ein. Die Methode wird von der Klasse »PHPUnit_Framework_TestCase« bereitgestellt, von der sich »Horde_Service_Gravatar_GravatarTest« ableitet, und ist entsprechend als »$this->assertType (…)« in der Testsuite verfügbar.
Der Ausdruck »$this->assertType(‘string’, $g->getId(‘test’));« behauptet, dass der Aufruf »$g->getId(‘test’)« einen Rückgabewert vom Typ String liefert. Ohne passende Implementierung schlägt dieser Test fehl, da nicht einmal die Funktion »getId(…)« in der Klasse »Horde_Service_Gravatar« definiert ist. Das lässt sich jedoch korrigieren:
public function getId($mail) { return ''; }
Diese Zeilen fügen eine triviale Implementierung für »getId(…)« hinzu. Natürlich ist diese Lösung naiv, aber sie erfüllt den Test: Es kommt stets ein String zurück und »phpunit GravatarTest.php« meldet zum ersten Mal (Abbildung 3) den erfolgreichen Durchlauf der Testsuite. In der Praxis wird der Entwickler meist größere Schritte machen und die Trivialimplementierung überspringen. Dennoch besteht die Möglichkeit, in solch kleinen Schritten zu arbeiten – etwa um schwierige Codepartien zu meistern.
Ein Test, eine Assertion
Der erste Test ist damit abgeschlossen. Eine wichtige Regel beim Unit-Testing besagt, dass jeder Test nur eine Assertion erhält. Das mag verwundern, zwingt es den Entwickler doch im vorliegenden Beispiel dazu, in jedem Test die Zeile »$g = new Horde_Service_Gravatar();« zu wiederholen, und das bei einem einfachen Setup. Stellt man sich komplexere Szenarien vor, mag es attraktiver erscheinen, den gesamten Setupcode für die Tests nur einmal niederzuschreiben und danach 50 Assertions innerhalb der Methode »public function testEverything()« folgen zu lassen.
Davon ist jedoch strikt abzuraten, da sich solche Strukturen deutlich schlechter pflegen lassen als das Modell “Ein Test – eine Assertion”. Bei der Weiterentwicklung des Code kann es durchaus passieren, dass sich das Testsetup ändert oder eine Assertion abgewandelt oder gar obsolet wird. Ein Geflecht von Abhängigkeiten zwischen Testsetup und Assertions erhöht die Gefahr, dass entsprechende Anpassungen kompliziert werden. Bringt jeder Test nur eine einzige und unabhängige Assertion mit, lässt er sich problemlos anpassen, ohne die anderen Tests zu beeinflussen.
Code, der für das Testsetup erforderlich ist und von mehreren Testmethoden verwendet werden kann, sollte der Programmierer in private Methoden der Testsuite auslagern. So vermeidet er exzessives Copy&Paste zwischen den einzelnen Testmethoden.
Im zweiten Test »testAddress()« überprüft »assertEquals(…)« , eine Methode die auf Gleichheit testet, den Hash, der für eine gegebene E-Mail-Adresse zurückgegeben wird. Die beiden bisher formulierten Tests lassen sich immer noch durch eine triviale Implementierung (»hgs_step3« ) zufriedenstellen:
public function getId($mail) { return '0c17bf66e649070167701d2d3cd71711'; }
Erst mit dem dritten Test, »testAddresses($mail, $id)« , wird es notwendig, den Produktivcode zu schreiben. Die Testmethode zeigt auch eine Besonderheit: Sie erwartet die zwei Argumente »$mail« und »$id« . Für jedes Tupel dieser Art wird überprüft, ob der Input »$mail« in der Gravatar-ID »$id« resultiert.
PHP Unit entnimmt die benötigten Argumente der Annotation »@dataProvider provideAddresses« im Dokumentations-Block der Funktion. Die Ausführung der Tests ruft die Funktion »provideAddresses()« auf und erwartet, dass ein Array aus Arrays mit Testargumenten zurückkommt. Die Iteration über das Array füttert die Testmethode mit je einem Satz an Argumenten. »provideAddresses« liefert hier noch drei Argument-Tupel, mit denen sich die »getId()« -Funktion auf kompakte Art und Weise mit variablen Eingaben testen lässt.
Dieser Test zwingt den Programmierer dazu, die »getId()« -Methode mit Logik zu füllen (»hgs_step4« ):
public function getId($mail) { return md5($mail); }
Der Kern der ID-Generierung entsprechend dem Gravatar-API ist damit umgesetzt. Das Erzeugen der IDs ignoriert allerdings noch Leerzeichen an Beginn und Ende der angegebenen Mailadresse und verwendet ausschließlich Kleinschreibung. Die erforderliche Testsequenz findet sich unter [4] (»hgs_step5« und »hgs_step6« ) und verändert »md5($mail)« zu »md5(strtolower(trim($mail)))« .
Annotations
Der Code finalisiert die »getId($mail)« -Methode noch mit der Überprüfung des »$mail« -Parameters und verwendet dazu die PHP-Unit-spezifische Annotation »@expectedAnnotation« . Diese erlaubt es, eine Fehlersituation beim Durchlaufen des Tests zu erwarten (»hgs_step7« und »hgs_step8« ). Abschließend folgt die Implementierung der zwei Hilfsfunktionen »getAvatarUrl($mail)« und »getProfileUrl($mail)« (»hgs_step9« und »hgs_step10« ).
Um Avatare sowohl über HTTP als auch HTTPS abzuholen, macht der Entwickler die Klasse unabhängig von einer fixen Basis-URL (»hgs_step11« und »hgs_step12« ). Schließlich sieht der testgetriebene Code der Klasse »Horde_Service_Gravatar« wie in Listing 2 aus.
Listing 2
Die Klasse Horde_Service_Gravatar
01 class Horde_Service_Gravatar 02 { 03 const STANDARD = 'http://www.gravatar.com'; 04 const SECURE = 'https://secure.gravatar.com'; 05 06 private $_base; 07 08 public function __construct($base = self::STANDARD) 09 { 10 $this->_base = $base; 11 } 12 13 public function getId($mail) 14 { 15 if (!is_string($mail)) { 16 throw new InvalidArgumentException('The mail address must be a string!'); 17 } 18 return md5(strtolower(trim($mail))); 19 } 20 21 public function getAvatarUrl($mail) 22 { 23 return $this->_base . '/avatar/' . $this->getId($mail); 24 } 25 26 public function getProfileUrl($mail) 27 { 28 return $this->_base . '/' . $this->getId($mail) . '.json'; 29 } 30 }
Die Funktionalität der Klasse wäre jetzt schon für die meisten Nutzungsfälle ausreichend, denn die Rückgabe von »getAvatarUrl($mail)« lässt sich problemlos in ein HTML-Image-Tag einbinden, um das einer Mailadresse zugehörige Avatar-Bild in eine Webseite einzubauen.
Störfaktoren ausschließen
Das Programmierbeispiel soll aber die nächste Hürde nehmen und direkt mit dem Gravatar-Server kommunizieren, um Profildaten oder das Avatar-Bild herunterzuladen. Dafür muss der Code zwangsläufig per Netzwerk kommunizieren, doch das sollte man innerhalb eines Unit-Tests vermeiden. Ein Test, der für den Erfolgsfall Daten von einem entfernten Server abholen muss, wird fehlschlagen, sobald keine Netzwerkverbindung besteht oder der entfernte Server nicht erreichbar ist.
Diese Umstände möchte ein Entwickler in seinem Unit-Test aber nicht überprüfen – ihm geht es ausschließlich um die Funktionalität seines eigenen Code und nicht um die Verkehrssituation im Internet. Daher muss er den Netzwerkzugriff umgehen und seinen Code trotzdem testbar machen.
Das gelingt am einfachsten, indem er den HTTP-Zugriff auf den entfernten Gravatar-Server in einer eigenen Klasse kapselt. Dazu greift er auf bestehenden Code zurück – das Paket »Horde_Http« . Mit Hilfe von PEAR lässt es sich folgendermaßen installieren:
pear channel-discover pear.horde.org pear install horde/Horde_Http
Das Paket »Horde_Http« stellt die Klasse »Horde_Http_Client« zur Verfügung. Sie lässt sich einsetzen, um den eigentlichen HTTP-Zugriff aus der Klasse »Horde_Service_Gravatar« herauszuhalten. Die Konstruktion
$c = new Horde_Http_Client(); $c->get('http://example.com')->getBody();
erlaubt es dabei, beliebige Webseiten auszulesen.
Mocking und Stubbing
Um den Zugriff auf die Profildaten zu simulieren, erzeugt der Programmierer Testdoubles und greift auf das von PHP Unit mitgelieferte Mocking- und Stubbing-Framework zurück. Stubs sind Objekte, die in einem Test bestimmte Rückgabewerte simulieren. Unter Mocks versteht man Objekte, die überprüfen, ob sie während des Tests in erwarteter Weise aufgerufen wurden.
Das Testsetup ist in Listing 3 wiedergegeben und sieht diesmal komplizierter aus. Der Hauptteil ist in die Methode »_getMockedGravatar($response_string)« ausgelagert. Als Erstes erzeugt der Code ein Testdouble für die HTTP-Antwort und simuliert in Zeile 12 die Methode »getBody()« mit (»$this->getMock(‘Horde_Http_Response’, array(‘getBody’))« ). PHP Unit erzeugt hier eine von der Originalklasse »Horde_Http_Response« abgeleitete Dummy-Instanz.
Listing 3
Simulierter Netzwerkzugriff
01 public function testGetProfile() 02 { 03 $g = $this->_getMockedGravatar('{"test":"example"}'); 04 $this->assertEquals( 05 array('test' => 'example'), 06 $g->getProfile('test@example.org') 07 ); 08 } 09 10 private function _getMockedGravatar($response_string) 11 { 12 $response = $this->getMock('Horde_Http_Response', array('getBody')); 13 $response->expects($this->once()) 14 ->method('getBody') 15 ->will($this->returnValue($response_string)); 16 17 $mock = $this->getMock('Horde_Http_Client', array('get')); 18 $mock->expects($this->once()) 19 ->method('get') 20 ->will($this->returnValue($response)); 21 22 return new Horde_Service_Gravatar( 23 Horde_Service_Gravatar::STANDARD, 24 $mock 25 ); 26 }
Der Test formuliert für diese Dummy-Instanz in Zeile 13 (Listing 3) die Erwartung, dass die Methode »getBody()« (»->method(‘getBody’)« ) einmal aufgerufen wird (»->expects($this->once())« ) und in diesem Fall den in »$response_string« vorgegebenen Wert zurückgibt (»->will($this->returnValue($response_ string))« ).
Für einen genaueren Überblick über das Mocking- und Stubbing-Framework von PHP Unit empfiehlt sich der entsprechende Abschnitt der Dokumentation [7]. Es gibt einige Kritik an dem komplizierten API dieses Framework, daher sei noch auf die alternative Mockery-Bibliothek von Pádraic Brady unter [8] verwiesen. Sie lässt sich problemlos mit PHP Unit verwenden.
Im zweiten Schritt ab Zeile 17 erzeugt der Entwickler in »_getMockedGravatar($response_string)« noch ein Double für das eigentliche HTTP-Client-Objekt. Es wird gleichfalls instruiert, in jedem Fall das vorher definierte Dummy-Response-Objekt zurückzugeben. Die Tests lassen sich mit der passenden Implementierung in »fetchProfile($mail)« ohne Netzwerkzugriff zum Laufen bringen:
public function fetchProfile($mail) { return $this->_client->get($this->getProfileUrl($mail) . '.json')->getBody(); }
Die in Listing 3 verwendete Methode »_getMockedGravatar($response_string)« ist leider nicht besonders lesefreundlich. Außerdem sind die automatisch generierten Mocks und Stubs von PHP Unit zwar leicht zu erstellen, sie führen aber schnell zu unübersichtlichen Mocking-Orgien, die langfristig nicht wartbar bleiben. Sobald die Testsuiten umfangreicher werden, empfiehlt es sich, eigene Stub-Klassen zu definieren und auf den von PHP Unit mitgelieferten Generator von Mock- oder Stub-Objekten zu verzichten.
Der Aufwand, eigene Stub-Klassen zu definieren, mag anfangs zwar größer sein, er rechnet sich aber langfristig. Außerdem führen selbst geschriebene Objekte zu deutlich realistischeren Tests, da die manuell erstellten Klassen höhere Komplexität abbilden können als die automatisch generierten. Wie sich die Mock-Klassen, die das »Horde_Http« -Paket von Haus aus mitbringt, verwenden lassen, um besser lesbare Tests zu generieren, demonstriert das Tag »hgs_step14« im Onlinebeispiel [4].
Abschließend findet sich dort auch noch eine zweite Testsuite, die den Netzwerkzugriff explizit nicht umgeht und so in der Lage ist, die Kommunikation des Code auch gegen den realen Gravatar-Server zu testen. Standardmäßig sind diese Tests jedoch abgeschaltet, sie lassen sich durch eine Konfigurationsdatei aktivieren. Wie man mehrere Testsuiten vereint, zeigt das Git-Tag »hgs_step15« .
Code-Coverage
Ein wichtiger Punkt bei der Anwendung von PHP Unit ist die Code-Coverage (Code-Abdeckung). Die Visualisierung jener Codebereiche, die bei den Tests durchlaufen werden, ist zwar kein Allheilmittel, hilft aber problematische Partien zu identifizieren. In der Regel finden sich in den nicht abgedeckten Bereichen deutlich mehr Fehler. Zudem lässt sich dieser Code schlechter warten, da Änderungen zu Fehlern führen können, die ohne Tests unentdeckt bleiben.
PHP Unit benötigt zum Generieren der Code-Coverage im HTML-Format die PHP-Erweiterung »xdebug« , die sich ohnehin im Werkzeugkasten eines jeden PHP-Entwicklers finden sollte. Ist sie installiert, lässt sich der Bericht über Code-Coverage mit dem Kommando »phpunit –coverage-html=Zielverzeichnis GravatarTest.php« im gewünschten Verzeichnis erzeugen. Abbildung 4 zeigt die generierte Übersicht.
Automatisieren
Wer seine Unit-Tests mit jedem Commit automatisch ausgeführt wissen will, der kann dies über Continuous Integration (CI) erreichen. Das ergibt vor allem für Entwickler Sinn, die in einem größeren Team arbeiten. So lässt sich sicherstellen, dass Änderungen am Code immer gegen die Testsuite validiert werden. Außerdem lassen sich bei problematischen Commits Warnungen an die Teammitglieder versenden, die den Fehler zeitnah korrigieren können.
Für PHP hat sich in letzter Zeit der Continuous-Integration-Server Jenkins (vormals Hudson, [9]) als Mittel der Wahl herauskristallisiert. Wie sich ein sinnvolles Setup für PHP-Projekte aufsetzten lässt, ist unter [10] in einfach nachvollziehbarer Weise beschrieben.
Abgeleitet von dieser Setup-Anleitung platziert auch das Horde-Projekt seine Pakete in einer durch Jenkins verwalteten Continuous-Integration-Umgebung unter http://ci.horde.org. Die Berichte zu dem hier demonstrierten Beispielpaket »Horde_Service_Gravatar« sind ebenfalls dort zu finden [11] und zudem in Abbildung 5 dargestellt.
PHP Unit ist kein Allheilmittel gegen schlecht wartbaren Code. Im Gegenteil, man kann sogar problemlos schlecht wartbaren Testcode schreiben. Zum effektiven Einsatz des Werkzeugs gehört – wie generell beim Programmieren – eine ganze Portion Erfahrung. Diese wiederum kann nur erlangen, wer sich intensiv mit dem Thema beschäftigt.
Testen muss sein
PHP Unit gestaltet den Einstieg ins Unit-Testing für PHP einfach. Für den professionellen PHP-Programmierer gibt es damit keine Ausreden mehr: Das Thema Testing muss ihm vertraut werden.
Infos
- PHP Unit: http://www.phpunit.de
- Horde: http://www.horde.org
- Gravatar-API: http://de.gravatar.com/site/implement/
- Beispielcode im Git-Repository: https://github.com/horde/horde/blob/hgs_step15/framework/Service_Gravatar/test/Horde/Service/Gravatar/GravatarTest.php
- PHP-Autoloading: http://groups.google.com/group/php-standards/web/psr-0-final-proposal
- Kent Beck, “Test Driven Development”: Addison-Wesley Professional, 2002
- Mocking und Stubbing mit PHP Unit: http://www.phpunit.de/manual/3.5/en/test-doubles.html
- Mockery-Bibliothek: https://github.com/padraic/mockery
- Carsten Zerbst, “Am Ball bleiben”: Linux-Magazin 07/10, S. 106
- Jenkins für PHP-Entwickler: http://jenkins-php.org
- Berichte zum Beispielcode: http://ci.horde.org/job/Service_Gravatar












