Angeregt von einem netten Kommentar zu meinem 2. Artikel zu nachhaltiger PHP Entwicklung (Dank an Thorsten Pohl nochmal!) habe ich mich eingehend mit Continuous Integration befasst. Seitdem hat mich das Konzept “Test-Driven-Development” infiziert. Es ist ein tolles Gefühl wenn man verifizieren kann ob eine Änderung ein unerwartetes Problem (vielleicht an ganz anderer Stelle) erzeugt. Man kann viel mutiger Änderungen vornehmen und verringert die Wahrscheinlichkeit 3 von 2 möglichen Fehler zu produzieren. Zusätzlich ist auch die Auswirkung auf den Stil der eigenen Programmierung durchaus positiv. Wirft man einen Blick auf die Auswertungen der Code Coverage, dem PHP Mess Detector und ähnlichem überlegt man hier und dort einmal mehr ob es auch anders geht, ob vielleicht eine weitere Methode sinnvoll wäre oder ob die eine oder andere Bedingung notwendig ist. Insgesamt halte ich den Mehraufwand nach einiger Übung für sinnvoll, speziell bei größeren Projekten die man etwas länger begleitet. Derzeit arbeite ich mich Schritt für Schritt durch meine Kopie des Buches Softwarequalität in PHP-Projekten von Sebastian Bergmann . Das Buch geht deutlich über Unittests hinaus und hat meine Sicht auf Software Entwicklung generell umgekrempelt. Eine absolute Bereicherung für jeden Entwickler. Eine klare Leseempfehlung an dieser Stelle. Aber, zurück zum Thema.
phpUnderControl
Die erste CI-Lösung mit der ich ein wenig experimentiert habe war wie empfohlen phpUnderControl. Irgendwie hat mir allerdings die Integration in ein Linuxsystem nicht so richtig gefallen. Ich hätte am liebsten gleich einiges anders gestaltet. So fehlte z.B. ein gutes Initskript, eine FHS-konforme Aufteilung der Daten und ähnliches. Ich bin wirklich nicht pedantisch was sowas angeht, aber es fühlte sich einfach “komisch” an die Daten gesammelt unter bspw. /opt zu “parken”. Ist keine stumpfe Kritik. Ich bin mir bewusst dass der Opensource Gedanke alle Wege öffnet meine eigenen Gedanken einzubringen. Leider fehlte mir derzeit, wie für soviele andere Projekte die ich gerne unterstützen würde, jedoch die Zeit. Wie der Zufall aber so will bin ich Anfang des Jahres durch die ganze Aufregung rund um Jenkins (ehemals Hudson) auf die Continuous Integration Lösung aus der Java Welt aufmerksam geworden. In der Hoffnung eine “out of the box Lösung” zu finden habe ich beschlossen Jenkins eine Chance zu geben. Was für eine tolle Idee!
Jenkins für PHP Projekte
Wie sich zeigte war ich nicht der Erste mit der Idee Jenkins für PHP Projekte zu verwenden. So bin ich unter jenkins-php.org auf ein Job Template von Sebastian Bergmann gestossen welches eine perfekte Vorlage für PHP Projekte darstellt. Dass Buildsystem basiert auf Ant, ein Tool aus der Java Welt dass sich ebenfalls für PHP Projekte gut eignet. Jenkins startet bei einem Testlauf Ant mit einem vorbereiteten Buildskript (build.xml). Verschiedene Tools wie z.B. der PHP CodeSniffer, PHP Mess Detector und PHPUnit werden von diesem wiederrum angestossen. Die bei einem Testlauf generierten Logdateien können anschließend zentral über dass Jenkins Webinterface dargestellt werden. Bei dem angesprochenen Projekt nutze ich derzeit einen SCM (in meinem Fall: Subversion) gesteuerten Trigger für Jenkins. Dieser erkennt Veränderungen (“commits”) in einem Repository und holt sich einen frischen “Checkout”. Anschließend wird Ant gestartet. Kommt es zu Fehlern, z.B. wenn PHPUnit Tests fehlschlagen, werde ich per E-Mail informiert. Dass Szenario ist in größeren Teams noch einmal weitaus praktischer als für einen einzelnen Entwickler. Dennoch ist es für mich eine unheimliche Bereicherung, da die zentrale und übersichtliche Erreichbarkeit der Auswertungenunschlagbar ist. Auch die automatische Benachrichtigung im Fehlerfall ist von Vorteil. Ich habe nun bereits mehrfach Verbesserungen/Änderungen am Code auf Basis der gewonnen Daten vorgenommen und Fehler entdeckt die auf meiner aktuelleren Workstation (Ubuntu 10.10) nicht aufgetreten sind. Fazit an dieser Stelle daher: Zur Nachahmung empfohlen!
Jenkins und dass Zend Framework
Die grundlegende Installation ist auf der Seite zum Jenkins PHP Template (jenkins-php.org) super beschrieben. Ist dann dass Template für den Jenkins Job aus dem Github Repository erst einmal ausgecheckt kann es auch schon losgehen. Hier konzentriere ich mich nun auf Zend Framework spezifische Ergänzungen und Hinweise. Das folgende Setup nutze ich selbst für die Anbindung von Zend Framework Projekten an Jenkins. Beginnen wir mit der Vorbereitung von Helferklassen die es uns später erleichtern Tests für dass Zend Framework Projekt zu schreiben. Als erstes ein “TestHelper” welcher bspw. dass automatische Laden von Klassen vorbereitet. Abgelegt wird dieser im Verzeichnis “tests” unter dem Namen TestHelper.php:
TestHelper.php
</p>
<p>// start output buffering<br /> ob_start();</p>
<p>// set our app paths and environments<br /> if (!defined('BASE_PATH')) {<br /> define('BASE_PATH', realpath(dirname(__FILE__) . '/../'));<br /> }<br /> if (!defined('APPLICATION_PATH')) {<br /> define('APPLICATION_PATH', BASE_PATH . '/application');<br /> }<br /> if (!defined('APPLICATION_ENV')) {<br /> define('APPLICATION_ENV', 'testing');<br /> }</p>
<p>// Include path<br /> set_include_path(<br /> '.'<br /> . PATH_SEPARATOR . BASE_PATH . '/library'<br /> . PATH_SEPARATOR . BASE_PATH . '/propel/models'<br /> . PATH_SEPARATOR . get_include_path()<br /> );</p>
<p>require_once 'Zend/Loader/Autoloader.php';<br /> $autoloader = Zend_Loader_Autoloader::getInstance();<br /> $autoloader->registerNamespace('Luckyduck_');</p>
<p>require_once 'Zend/Loader/Autoloader/Resource.php';<br /> $resources = new Zend_Loader_Autoloader_Resource(array(<br /> 'namespace' => 'Application',<br /> 'basePath' => APPLICATION_PATH<br /> ));<br /> $resources->addResourceType('form','forms','Form');<br /> $resources->addResourceType('model','models','Model');<br /> $resources->addResourceType('dbtable','models/DbTable','Model_DbTable');</p>
<p>// Set the default timezone !!!<br /> date_default_timezone_set('Europe/Berlin');</p>
<p>// We wanna catch all errors en strict warnings<br /> error_reporting(E_ALL|E_STRICT);</p>
<p>require_once 'ControllerTestCase.php';<br />
Sowie eine abstrakte Klasse ControllerTestCase. Diese stellt die Basis für die eigenen Unittests dar. Abzulegen ist sie im Verzeichnis “tests” in der Datei ControllerTestCase.php:
</p>
<p>< ?php<br /> require_once 'Zend/Application.php';<br /> require_once 'Zend/Test/PHPUnit/ControllerTestCase.php';</p>
<p>abstract class ControllerTestCase extends Zend_Test_PHPUnit_ControllerTestCase<br /> {<br /> public $application;<br /> public function setUp()<br /> {<br /> $this->application = new Zend_Application(<br /> 'testing',<br /> APPLICATION_PATH . '/configs/application.ini'<br /> );</p>
<p>$this->bootstrap = array($this, 'appBootstrap');<br /> parent::setUp();<br /> }</p>
<p>public function appBootstrap()<br /> {<br /> $this->application->bootstrap();<br /> }<br /> }</p>
<p>?></p>
<p>
Hier sind ein paar projektspezifische Details zu beachten. Z.b. wird in der Datei TestHelper.php der Namespace “Luckyduck_” beim Autoloader registriert. Ggf. kann man hier eigene Namespaces (Achtung: nicht PHP 5.3 Namespaces) eintragen oder evt. ganz darauf verzichten. In meinem Fall war es trotz Eintrag in der application.ini (wird im ControllerTestCase geladen) notwendig. Zusätzlich muss evt. auch der Includepfad angepasst werden, da ich hier von Propel generierte Models im Verzeichnis “propel/models” zum Includepath hinzufüge.
Im ControllerTestCase wird dann von einer Klasse aus dem Zend_Test Paket geerbt. Der Zend_Test_PHPUnit_ControllerTestCase ist die Grundlage für Tests von “Controllern”. Mit seiner Hilfe sind sogar ganze Systemtests möglich. Der Aufruf von einzelnen URLs einer Zend Framework Applikation kann simuliert werden. Die Applikation verarbeitet die Anfrage dann als zusammenhängendes System:
- Router leitet die Anfragen zu passender Stelle (Module, Controller, Action)
- Eine Action verarbeitet bspw. ein Formular
- Man könnte die Datenbank testen (lesend/schreibend)
- Ist ein Fehler aufgetreten (z.B. beim ErrorController “gelandet”?)
- usw.
Im ControllerTestCase wird die Applikation ebenfalls initialisiert (ich fand dass Wort “gebootstrapped?!” zweifelhaft), ohne dabei jedoch die finale Methode “run()” auszuführen. Was nun fehlt ist ein Test der wirklich etwas macht.
Ein Unittest
Um zu verifizieren ob alles eingerichtet ist fehlt der erste Unittest. Hierfür können wir z.B. wie folgt prüfen ob der Aufruf einer nicht vorhandenen URL eine passende Antwort erzeugt (ErrorController / HTTP Response Code 404). Damit die Pfadangabe oben stimmt muss die Datei im Verzeichnis “tests/application/controllers” liegen. Genannt habe ich Sie IndexControllerTest.php:
</p>
<p>< ?php</p>
<p>require_once realpath(dirname(__FILE__) . '/../../ControllerTestCase.php');</p>
<p>class IndexControllerTest extends ControllerTestCase<br /> {<br /> public function testCallingBogusTriggersError()<br /> {<br /> $this->dispatch('/bogus');<br /> $this->assertModule('default');<br /> $this->assertController('error');<br /> $this->assertAction('error');<br /> $this->assertResponseCode(404);<br /> }<br /> }</p>
<p>?></p>
<p>
PHPUnit Konfiguration
Die Konfigurationsdatei für PHPUnit, bzw. die Angaben zum Logging, sind auf jenkins-php.org bereits vorgegeben. Die Jenkins Module welche unsere Logdateien verarbeiten erwarten ein bestimmtes Format. Die Konfiguration des Loggings allein reicht jedoch noch nicht aus um wirklich dass eigene Zend Framework Projekt von Jenkins prüfen zu lassen. Hier meine PHPUnit Konfiguration welche ich im Stammverzeichnis abgelegt habe. Der Name der Datei ist “phpunit-jenkins.xml”:
</p> <p>colors="true"<br /> verbose="true"<br /> stopOnFailure="true"<br /> processIsolation="true"<br /> backupGlobal="false"<br /> syntaxCheck="true"></p> <p>tests/library<br /> tests/application</p> <p>./library/Luckyduck<br /> ./application</p> <p>./application</p> <p>charset="UTF-8" yui="true" highlight="true"<br /> lowUpperBound="35" highLowerBound="70"/></p> <p>
Hier wird der “TestHelper” als Bootstrapdatei verwendet und ein paar Einstellung bzgl. PHPUnit angepasst. Diese waren auf meinem Produktivsystem notwendig da dort unter Ubuntu 10.04 LTS eine etwas ältere PHPUnit Version zum Einsatz kommt. Ein Update über PEAR hat mir nicht kurzfristig zu lösende Probleme bereitet, weshalb ich dann (vorerst) zur Ubuntu Version zurückgerudert bin. Im Großen und Ganzen werden durch diese Konfiguration alle Dateien mit der Endung “.php” unter dem application-Verzeichnis in die Tests und Code Coverage Auswertung mit einbezogen. Zusätzlich teste ich noch meine Bibliothek unter “library/Luckyduck” . Leider reichten 2G memory_limit noch immer nicht aus um auch dass Zend Framework selbst in die Unittests (zumindest mal testweise) einzubeziehen. Sollte es wegen Arbeitsspeicher zu Problemen kommen als erstes kontrollieren ob ggf. dass komplette “library” Verzeichnis einbezogen wird.
Ant Buildskript
Hier nun im letzten Schritt dann noch dass Ant Buildskript. Auch dies habe ich ein wenig modifiziert. Speziell wichtig ist hier die Modifikation “phpunit”-Target. Dort wird nun auf der Kommandozeile der Name der PHPUnit Konfiguration (phpunit-jenkins.xml) übergeben. Zusätzlich interessant ist vielleicht noch dass Target “dist” welches ich verwende um Releases zu erstellen. Dazu ersetze ich vor einem Aufruf einfach ganz oben die Versionsnummer und lasse mir anschließend ein ZIP-Archiv erzeugen. In diesem sind dann nur gewünschte Daten enthalten. Ich wollte hier gerne noch ein Subversion Target einbauen welches dann zeitgleich dass Repository mit der Versionsnummer “tagged”, aber auch dazu fehlte bisher einfach wieder die Zeit. Die Datei build.xml liegt auf gleicher Ebene wie die Datei phpunit-jenkins.xml, im Stammverzeichnis meines Projekts.
<br /> <!-- Clean up --></p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p><!-- copy propel template --><br /> tofile="${basedir}/dist/build/propel/configs/anzeigenmarkt-conf.php"<br /> overwrite="true"/></p>
<p><!-- zipfile --><br /> basedir="${basedir}/dist/build"<br /> update="true"<br /> level="9"<br /> /></p>
<p>
Abschliessend
Ist dann die Einrichtung abgeschlossend und alle Dateien an der richtigen Stelle, sollte die Verzeichnisstruktur (neben den ohnehin vorhandenen Daten) in etwa wie folgt aussehen. Wichtig sind hier natürlich vor allem die erwähnten Dateien und Verzeichnisse:
Nun muss nur noch dass Projekt inkl. der neuen Dateien ins Repository übertragen bzw. “commited” werden. Wird der SCM Trigger eingesetzt sollte Jenkins schon bald den ersten Testlauf starten. Zudem kann allerdings für unmittelbare Tests auch die Funktion “build now” genutzt werden. So erhält man unmittelbar Feedback. Über Anregungen, Tipps und Hinweise sowie über interessante Diskussionen hierzu würde ich mich selbstverständlich freuen. Ich bin mir sicher dass es in Zukunft weitere Beiträge zum Thema Unittesting geben wird. Derzeit befasse ich mich intensiv mit Magento, was vielleicht eine interessante Kombination ergeben könnte. Vielen Dank für’s lesen. Über Tweets und Empfehlungen würde ich mich selbstverständlich freuen!
Externe Links
Hier ein paar externe Links zu Jenkins, dem Zend Framework und Unittesting allgemein:
- Jenkins PHP Template
- Jenkins Homepage
- Zend Framework: Zend_Test
- Unit testing Zend Framework controllers
- Newbies guide to test driven development
- 10 Gründe die für dass Zend Framework sprechen







Ich wurde gefragt wieso kein public Verzeichnis existiert:
Der Grund dafür ist dass die Software möglichst mit jedem Shared-Hosting kompatibel sein sollte. Es gab mehrere Probleme mit suboptimalen Hostingprovidern welche keinerlei private Verzeichnisse bereitgestellt haben. Somit war es erwünscht alles ins DocumentRoot zu legen. Die “normalerweise” außerhalb liegenden Verzeichnisse wie bspw. application, library usw. sind über .htaccess Dateien geschützt (deny from all).
Danke für’s Fragen, Sven!
Wie sieht deine build.properties aus? ich denke mal in der hast du deine “${source}” definiert? Bezieht sich ${source} nur auf ein Verzeichnis (Bsp. (/application/*)? Falls es mehrere sind, wie kann ich die zentral definieren? Je nach target kann man entweder komma getrennte source verzeichnisse angeben oder per –exclude andere ausschliessen.
Hallo. Vielen Dank für den Kommentar! Zu Deiner Frage:
Ganz genau. In der build.properties ist ${source} definiert:
source=${basedir}/application${basedir} kommt aus der build.xml. Wie man mehrere zentral definiert weiß ich nicht genau. Darauf war ich bisher noch nicht angewiesen.
Hey Jan, schön das du das alles soweit erklärst und es tut mir leid das ich jetz so ankomme aber ich komm irgend wie nicht weiter…. und zwar hab ich schon auf verschiedenen Arten probiert das zu machen und jedes mal sagt er mir:
Fehler: Kein Arbeitsbereich.
Ein Projekt besitzt keinen Arbeitsbereich, so lange nicht mindestens ein Build ausgeführt wurde.
Starten Sie einen Build, um von Jenkins einen Arbeitsbereich anlegen zu lassen.
“Starten Sie einen Build” ist zwar ein Link aber wenn ich drauf drücke passiert nichts und ich verzweifel langsam dran, kannst du mir weiterhelfen und sagen was ich falsch mache?
Danke schon mal im vorraus.
Hallo. Das Problem hatte ich in der Form nicht. Daher kann ich ohne das System im Detail zu kennen oder wenigstens gesehen zu haben nur bedingt helfen. Gerne schaue ich aber wenn Du möchtest mal drauf, vielleicht entdecke ich etwas. Das jedoch der Link nicht funktioniert mit dem der Buildprozess gestartet wird, klingt komisch. Du kannst mir gerne Details per E-Mail schicken wenn Du die nicht veröffentlichen möchtest.
Ich denke mein Problem liegt bei Ant:
/home/horst/.jenkins/jobs/myproject/build.xml:18: Execute failed: java.io.IOException: Cannot run program “phpab”: java.io.IOException: error=2, No such file or directory
Wenn ich bei executable “phpab” reinschreibe kommt diese Fehlermeldung, wenn ich da aber z.b. “phpmd” reinschreibe kommt wieder was ganz anderes:
phpmd:
[exec] Can’t find the custom report class: /home/horst/.jenkins/jobs/myproject/src/autoload.php
[exec] Result: 1
[exec] Can’t find the custom report class: /home/horst/.jenkins/jobs/myproject/tests/autoload.php
[exec] Result: 1
Ich würde dir ja gern mal alles schicken aber ich sehe hier nirgendswo deine E-Mail-Adresse, bin ich blind?
achja was ja auch noch witzig ist, ich habe alles von Sebastian Bergmanns “bankaccount”-Datei runtergeladen und kann diese auch mit mit meinem Jenkins ausführen. Ich sehe Sie nicht mal wie die “php-template” in der Übersicht.
Ach misst sorry das ich dich zuspam ich meine natürlich “nicht” ausführen.