1. PHPUnit
Eine kurze Einführung
vom Frank Staude <staude@trilos.de>
vorgetragen beim Treffen der
PHP Usergroup Hannover
am 02.11.2006
Warum Testen?
Jeder von uns, der programmiert, macht auch Fehler. Es gibt zwar Anwendungsbereiche in
der Luft- und Raumfahrt, wo die Fehlerfreiheit von Programmen mathematisch bewiesen
werden muss, aber auch dies ist noch kein Garant für das Funktionieren eines gesamten
Systems. Die Mars-Sonde „Climate-Orbiter“1 der NASA zum Beispiel verglühte in der
Marsatmosphäre, weil zwei Subsysteme zur Steuerung in verschiedenen Maßeinheiten
arbeiteten und die Einheit nicht Bestandteil der Schnittstellenspezifikation war.
Schaden: über 100 Millionen US-Dollar.
Dies zeigt auch, dass, wenn eine Software an sich fehlerfrei ist, das System als Ganzes
trotzdem versagen kann, weil die Umgebung der Software fehlerhaft ist. Für uns als PHP-Programmierer
bedeutet dies: Auch wenn unser Programm fehlerfrei ist, kann die Anwendung
als Ganzes Abstürzen, weil in einer bestimmten Situation ein Fehler in PHP selbst, im
verwendeten Webserver oder auch im Betriebssystem auftritt. Der Anwender gibt uns die
Schuld. Wir müssen mit viel Aufwand versuchen, die Situation nachzustellen, in der dieser
Fehler auftritt und – wenn wir feststellen, dass der Fehler durch die Umgebung verursacht
wird –, wie wir ihn umgehen können.
Da mathematische Korrektheitsbeweise für uns in der Regel nicht in Frage kommen, bleiben
nur Tests, um sicherzustellen, dass ein Programm fehlerfrei im Sinne der Spezifikation ist.
Damit Tests auch wirklich eingesetzt werden, müssen sie
• leicht zu erlernen sein.
Wir sind PHP Programmierer, wir wollen nicht erst eine weitere Scriptsprache eines
Testtools lernen.
• leicht zu schreiben sein.
Wenn ein Test nicht leicht zu schreiben ist, werden wir ihn nicht schreiben.
• leicht zu lesen sein.
Der Testcode sollte nichts überflüssiges eEnthalten (müssen).
• leicht auszuführen sein.
Ein Klick auf einen Button oder ein Aufruf an der Kommandozeile – und dann sollen
Ergebnisse kommen.
1 http://de.wikipedia.org/wiki/Mars_Climate_Orbiter
2. • schnell auszuführen sein.
Die Tests sollen so schnell sein, dass man sie viele, viele Male am Tag ausführen
kann.
• isoliert sein.
Tests dürfen sich nicht gegenseitig beeinflussen. Auch bei veränderter Reihenfolge der
Aufrufe müssen die Ergebnisse gleich sein.
• kombinierbar sein.
Jede beliebige Anzahl sowie Kombinationen von Tests können zusammen
durchgeführt werden.
Da nach jeder noch so kleinen Änderung in einem großen Projekt nicht ein Programmierer
alles manuell testen kann, müssen die Tests automatisiert sein.
Automatische Tests
Genau genommen ist ein automatischer Test nichts weiter als ein Vergleich zwischen einem
berechneten Ergebnis und einem vorab berechneten Wert. Wenn beide identisch sind, war der
Test erfolgreich, andernfalls nicht.
Solch ein automatisierter Test lässt sich jederzeit ohne Mehraufwand wiederholen und zeigt
keine Ermüdungserscheinungen, übersieht nicht mal etwas aus versehen. Und mit jedem
Durchlauf gewinnt man mehr Vertrauen in den eigenen Code.
Die Idee der automatischen Tests stammt von den so genannten agilen Methoden2 zur
Softwareentwicklung wie z.B. Extreme Programming (XP)3. Sehr interessant ist auch der
Ansatz der testgetriebenen Entwicklung4, bei der zu erst der Test geschrieben wird und erst
dann der Code. Das heißt, dass man mit dem Programmieren fertig ist, wenn der Test
fehlerfrei durchläuft. Diese Art der Arbeit zwingt einem zu sehr zielgerichteten Vorgehen.
Man muss sich von Anfang an mehr auf die Schnittstellen konzentrieren und schreibt bei der
Implementierung noch so viel Code, wie nötig ist, damit der Test fehlerfrei durchlaufen wird.
PHPUnit
PHPUnit2 ist die Vollständige Umsetzung von JUnit 3.8.1 nach PHP5. Sebastian Bergmann
hat es während seines Informatikstudiums entwickelt. PHPUnit2 ist die aktuelle, unter PHP5
lauffähige Version. PHPUnit ist die Version für PHP4, sie kann einige Dinge nicht und wird
auch nicht mehr weiterentwickelt.
Nur kurz angerissen, ohne im Rahmen dieses Vortrags weiter darauf einzugehen, die
folgenden Features von PHPUnit2:
Unterstützung unvollständiger Tests
Erstellen der Testdokumentation
Erstellen von Testcode-Skeletten (Skeletons)
Analyse der Codeabdeckung (mit xdebug)
Profiling (Performancetest)
2 http://de.wikipedia.org/wiki/Agile_Softwareentwicklung
3 http://de.wikipedia.org/wiki/Extreme_Programming
4 http://de.wikipedia.org/wiki/Testgetriebene_Entwicklung
3. Protokollierung der Testergebnisse als XML, TAP oder GraphViz
Installation
PHPUnit ist Bestandteil von PEAR und daher unter Linux und Windows gleichartig zu
installieren. Anwender des beliebten XAMPPs Paketes haben alles, was sie benötigen, schon
dabei.
pear version
PEAR Version: 1.4.5
PHP Version: 5.1.6
Zend Engine Version: 2.1.0
Running on: Windows NT MUNCH 5.1 build 2600
pear install phpunit2
Skipping package pear/PHPUnit2, already installed as version 2.3.5
No valid packages found
install failed
Unit Tests
Mit Unit Tests werden Klassen oder Teile einer Anwendung getestet, aber nicht die komplette
Anwendung. Durch das Ausführen der Tests an der Kommandozeile beispielsweise die GET
und POST Parameter – ebenso wie superglobale Variablen oder andere Umgebungsvariablen,
die der Webserver zur Verfügung stellt. Zum Testen der ganzen Anwendung und des
Interfaces im Browser sei hier auf Selenium5 verwiesen (bzw. die Einführung im Linux
Magazin6 ). PHPUnit wird derzeit auch aufgebohrt7, so dass man auch Selenium-Tests mit
PHPUnit realisieren kann.
Es gibt zwei Arten, die Tests zu organisieren. Entweder schreibt man die Tests zusammen mit
dem zu testenden Code in eine Datei oder man schreibt die Tests in separate Dateien. Schreibt
man die Tests mit in den Code, so ist man in der Lage, auch nicht-öffentliche Methoden zu
testen, auf die man von außen nicht zugreifen kann. Meiner Meinung nach sollten sich die
Tests aber auf die Schnittstelle konzentrieren und nicht die Implementierungsdetails testen.
Außerdem kann bei PHP nicht per Präprozessor-Anweisung der Testcode weggelassen
werden, so dass auch die ausgelieferte Anwendung immer den Testcode „mitschleppt“. Und
durch Trennung von Test- und Produktionscode hat man zusätzlich noch das gute Gefühl,
dass der Test „nicht angefasst wurde“ wenn man Refactorings8 vornimmt.
5 http://www.openqa.org/selenium/
6 http://www.linux-magazin.de/Artikel/ausgabe/2006/10/perl/perl.html
7 http://sebastian-bergmann.de/archives/631-Integrating-PHPUnit-with-Selenium.html
8 http://de.wikipedia.org/wiki/Refactoring
4. Ein erster Test
Angenommen, wir haben eine kleine Klasse UnitDemo, in der eine Methode isString()
implementiert ist. Diese wollen wir nun testen.
?php
class UnitDemo
{
function isString($s)
{
return (is_string($s));
}
}
?
Dazu benötigen wir einen Test.
?php
require_once(PHPUnit2/Framework/TestCase.php);
require_once(UnitDemo.php);
class UnitDemoTest extends PHPUnit2_Framework_TestCase
{
public function testIsString()
{
$this-assertTrue(UnitDemo::isString(test));
}
}
?
Die Ausführung des Tests liefert das folgende Ergebnis
phpunit UnitDemoTest.php
PHPUnit 2.3.5 by Sebastian Bergmann.
.
Time: 0.033367
OK (1 test)
Schauen wir uns den Test mal genauer an:
require_once(PHPUnit2/Framework/TestCase.php);
require_once(UnitDemo.php);
Von der TestCase-Klasse werden die Tests abgeleitet. Unsere zu testende Klasse muss
natürlich auch bekannt sein,
class UnitDemoTest extends PHPUnit2_Framework_TestCase
erstellt eine neue, von PHPUnit2_Framework_TestCase abgeleitete Klasse.
In dieser Klasse werden die einzelnen Testes als Methoden implementiert.
5. $this-assertTrue(UnitDemo::isString(test));
assertTrue (und auch das Gegenstück assertFalse) sind Tests auf Zusicherung (assertion) eines
Wertes. Das heißt, das Beispiel sichert zu, dass der Aufruf der Methode isString mit dem
Parameter „test“ ein True liefert.
Dem Testergebnis wird für jeden Test der Status zurückgebeben:
Ein Punkt bedeutet OK, so wie in obigen Beispiel. F wird ausgegeben, wenn eine
Zusicherung verletzt wurde.
E wird ausgegeben, wenn bei dem Test ein Fehler aufgetreten ist und
I, wenn der Test als unvollständig markiert wurde oder noch nicht Implementiert
wurde.
Wenn wir also den Test erweitern um eine falsche Zusicherung:
?php
require_once(PHPUnit2/Framework/TestCase.php);
require_once(UnitDemo.php);
class UnitDemoTest extends PHPUnit2_Framework_TestCase
{
public function testIsString()
{
$this-assertTrue(UnitDemo::isString(test));
}
public function testIsStringWithNumber()
{
$this-assertFalse(UnitDemo::isString(13));
}
}
?
erhalten wir im Test wie erwartet eine Fehlermeldung:
phpunit UnitDemoTest.php
PHPUnit 2.3.5 by Sebastian Bergmann.
.F
Time: 0.000689
There was 1 failure:
1) testIsStringWithNumber(UnitDemoTest)
C:Dokumente und EinstellungenFrank StaudeunitUnitDemoTest.php:15
FAILURES!!!
Tests run: 2, Failures: 1, Errors: 0, Incomplete Tests: 0.
Der zweite Test ist wie erwartet fehlgeschlagen. Die beiden AssertTrue/-False Funktionen
haben übrigens noch einen weiteren Parameter: Einen optionalen Text, der ausgegeben wird,
wenn die Zusicherung fehlschlägt. Bei umfangreichen Tests kann es sinnvoll sein, hier eine
6. Erläuterung zu geben, welche Zusicherung fehlgeschlagen ist – so kann man der Teststatistik
bereits entnehmen, wo etwas schiefgelaufen ist.
Zusicherungen
Die folgenden Zusicherungen gibt es:
assertTrue(Boolean $condition) Meldet einen Fehler, falls $condition
den Wert FALSE hat.
assertFalse(Boolean $condition) Meldet einen Fehler, falls $condition
den Wert TRUE hat.
assertNull(Mixed $variable) Meldet einen Fehler, falls $variable
nicht den Wert NULL hat.
assertNotNull(Mixed $variable) Meldet einen Fehler, falls $variable den
Wert NULL hat.
assertSame(Object $expected, Object
$actual)
Meldet einen Fehler, falls die beiden
Variablen $expected und $actual nicht
dasselbe Objekt referenzieren.
assertNotSame(Object $expected, Object
$actual)
Meldet einen Fehler, falls die beiden
Variablen $expected und $actual
dasselbe Objekt referenzieren.
assertEquals(Array|Float|String|Mixed
$expected, Array|Float|String|Mixed
$actual)
Meldet einen Fehler, falls die beiden
Array|Float|String|Mixed
$expected und $actual nicht identisch
sind.
assertNotEquals(Array|Float|String|Mixed
$expected, Array|Float|String|Mixed
$actual)
Meldet einen Fehler, falls die beiden
Array|Float|String|Mixed
$expected und $actual identisch sind.
assertContains(Mixed $needle, Array
$haystack)
Meldet einen Fehler, falls $needle nicht
in $haystack enthalten ist.
assertNotContains(Mixed $needle, Array
$haystack)
Meldet einen Fehler, falls $needle in
$haystack enthalten ist.
assertRegExp(String $pattern, String
$string)
Meldet einen Fehler, falls $string nicht
dem Regulären Ausdruck $pattern
entspricht.
assertNotRegExp(String $pattern, String
$string)
Meldet einen Fehler, falls $string dem
Regulären Ausdruck $pattern
entspricht.
assertType(String $expected, Mixed
$actual)
Meldet einen Fehler, falls die Variable
$actual keinen Wert vom Typ
$expected enthält.
assertNotType(String $expected, Mixed
$actual)
Meldet einen Fehler, falls die Variable
$actual einen Wert vom Typ $expected
enthält.
Jede der aufgeführten Methoden hat noch einen weiteren, optionalen Parameter für den
Hinweistext.
Desweiteren kann man auch noch auf exceptions testen, mit setUp() und tearDown() seine
Testumgebung beeinflussen und, wie am Anfang beschrieben, Testskelette erzeugen lassen
7. und CodeCoverage und Laufzeitanalyse machen – all dies würde aber den Rahmen einer
„kleinen“ Einführung sprengen. Der Interessierte sei hier an das frei verfügbare PHPUnit
kurz gut verwiesen, welches unter
http://www.phpunit.de/pocket_guide/index.de.php
im Internet zu finden ist bzw. von O’Reilly in gedruckter Fassung vorliegt.
Literatur Links
Homepage PHPUnit
http://www.phpunit.de/
PHPUnit kurz gut
http://www.phpunit.de/pocket_guide/index.de.php
Homepage von Sebastian Bergmann
http://sebastian-bergmann.de/
Professionelle Softwareentwicklung mit PHP 5
http://www.professionelle-softwareentwicklung-mit-php5.de/erste_auflage/
Der Autor
1970 geboren. Gründer und geschäftsführender Gesellschafter der
TRILOS® IT-Dienstleistungen GbR9.
Anschrift:
TRILOS IT-Dienstleistungen
Frank Staude
Am Rathaus 15
30952 Ronnenberg
Tel: (05 11) 21 44 98 - 60
Fax: (05 11) 21 44 98 - 65
E-Mail: staude@trilos.de
Webseite: http://www.trilos.de
PGP-Fingerprint: 1504 336A D8ED A2FD F189 996D DBBE 5A79 A9F2 E6C0
9 http://www.trilos.de