Continuous Integration mit Jenkins, PHPUnit und dem Zend Framework

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.

Inhalt

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



// start output buffering
 ob_start();



// set our app paths and environments
 if (!defined('BASE_PATH')) {
 define('BASE_PATH', realpath(dirname(__FILE__) . '/../'));
 }
 if (!defined('APPLICATION_PATH')) {
 define('APPLICATION_PATH', BASE_PATH . '/application');
 }
 if (!defined('APPLICATION_ENV')) {
 define('APPLICATION_ENV', 'testing');
 }



// Include path
 set_include_path(
 '.'
 . PATH_SEPARATOR . BASE_PATH . '/library'
 . PATH_SEPARATOR . BASE_PATH . '/propel/models'
 . PATH_SEPARATOR . get_include_path()
 );



require_once 'Zend/Loader/Autoloader.php';
 $autoloader = Zend_Loader_Autoloader::getInstance();
 $autoloader->registerNamespace('Luckyduck_');



require_once 'Zend/Loader/Autoloader/Resource.php';
 $resources = new Zend_Loader_Autoloader_Resource(array(
 'namespace' => 'Application',
 'basePath' => APPLICATION_PATH
 ));
 $resources->addResourceType('form','forms','Form');
 $resources->addResourceType('model','models','Model');
 $resources->addResourceType('dbtable','models/DbTable','Model_DbTable');



// Set the default timezone !!!
 date_default_timezone_set('Europe/Berlin');



// We wanna catch all errors en strict warnings
 error_reporting(E_ALL|E_STRICT);



require_once 'ControllerTestCase.php';
 

 

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:




< ?php
 require_once 'Zend/Application.php';
 require_once 'Zend/Test/PHPUnit/ControllerTestCase.php';



abstract class ControllerTestCase extends Zend_Test_PHPUnit_ControllerTestCase
 {
 public $application;
 public function setUp()
 {
 $this->application = new Zend_Application(
 'testing',
 APPLICATION_PATH . '/configs/application.ini'
 );



$this->bootstrap = array($this, 'appBootstrap');
 parent::setUp();
 }



public function appBootstrap()
 {
 $this->application->bootstrap();
 }
 }



?>



 

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:

 



< ?php



require_once realpath(dirname(__FILE__) . '/../../ControllerTestCase.php');



class IndexControllerTest extends ControllerTestCase
 {
 public function testCallingBogusTriggersError()
 {
 $this->dispatch('/bogus');
 $this->assertModule('default');
 $this->assertController('error');
 $this->assertAction('error');
 $this->assertResponseCode(404);
 }
 }



?>



 

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”:



colors="true"
 verbose="true"
 stopOnFailure="true"
 processIsolation="true"
 backupGlobal="false"
 syntaxCheck="true">



tests/library
 tests/application



./library/Luckyduck
 ./application



./application



charset="UTF-8" yui="true" highlight="true"
 lowUpperBound="35" highLowerBound="70"/>



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.

 

 <!-- Clean up -->


 



 



 



 



 



 



 



 



 



 



 



 



 



 



 



<!-- copy propel template -->
 tofile="${basedir}/dist/build/propel/configs/anzeigenmarkt-conf.php"
 overwrite="true"/>



<!-- zipfile -->
 basedir="${basedir}/dist/build"
 update="true"
 level="9"
 />



 

Abschließend

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: