Modulprogrammierung 5: Refactoring und mehr
Aus PapayaCMS
|
Tutorial |
|
| Zusammenfassung | Dieses Tutorial beschreibt, wie Sie Code-Dopplungen durch Refactoring loswerden und wie Sie Test-Suites bereitstellen. |
| Zielgruppe | PHP-Entwickler |
| Schwierigkeitsgrad | Fortgeschrittene |
| Softwarevoraussetzungen | SVN-Checkout von papaya CMS; Anleitung hier PHPUnit >= 3.5 für Unit-Tests |
| Datum | 2011-04-19 |
| Vorheriges Tutorial | Modulprogrammierung 4: Frontend-Interaktivität |
In diesem letzten Teil der Tutorial-Serie werden wir ein wenig Refactoring durchführen, um doppelte Implementierungen loszuwerden. Nachdem alles an seinem Platz ist, werden wir Test-Suite-Dateien hinzufügen, um die Ausführung aller Unit-Tests zu steuern und die Code-Coverage zu messen.
Bitte beachten Sie, dass Sie sich an die Papaya CMS Coding Standards halten sollten, besonders, wenn Sie vorhaben, Ihre Module der papaya-Community zur Verfügung zu stellen.
In diesem Tutorial werden Unit-Tests verwendet, eine sehr empfehlenswerte Methode der Softwareentwicklung. Damit Sie ein papaya CMS mit dem PHPUnit-Framework und der Klasse PapayaTestCase haben, benötigen Sie einen SVN-Checkout des Systems. Wie Sie ihn erhalten, erfahren Sie hier.
Inhaltsverzeichnis |
Übersicht über Dateien und Verzeichnisse
Nach dem Durcharbeiten dieses Tutorials werden Sie in papaya-lib/modules/special/myproject/tutorial die folgenden Dateien und Unterverzeichnisse erstellt haben (neue Dateien aus dem aktuellen Teil sind fett dargestellt):
+ [DATA] | | | + table_tutorial_planets.xml | | | + table_tutorial_planet_ratings.xml | + edmodule_tutorial_hello.php | + [Hello] | | | + Connector.php | | | + Output.php | | | + [Page] | | | | | + Base.php | | | + Page.php | | | + [Planet] | | | | | + Admin.php | | | | | + [Database] | | | | | | | + Access.php | | | | | + [Rating] | | | | | | | + [Box] | | | | | | | | | + Base.php | | | | | | | + Box.php | | | | | | | + [Database] | | | | | | | + Access.php | | | | | + Rating.php | | | + Planet.php | + modules.xml --------------------------------------- papaya-data/templates/default-xhtml/html/box_planet_rating.xsl Hinweis: Namen in eckigen Klammern bezeichnen Verzeichnisse
Im Verzeichnis testing/tests-unittests/papaya-lib/modules/special/myproject/tutorial werden Sie die folgende Struktur erstellt haben (neue Dateien aus dem aktuellen Teil sind fett dargestellt):
+ AllTests.php | + [Hello] | + AllTests.php | + ConnectorTest.php | + OutputTest.php | + [Page] | | | + AllTests.php | | | + BaseTest.php | + [Planet] | | | + AdminTest.php | | | + AllTests.php | | | + [Database] | | | | | + AccessTest.php | | | | | + AllTests.php | | | + [Rating] | | | | | + AllTests.php | | | | | + [Box] | | | | | | | + AllTests.php | | | | | | | + BaseTest.php | | | | | + BoxTest.php | | | | | + [Database] | | | | | + AccessTest.php | | | | | + AllTests.php | | | + RatingTest.php | + PlanetTest.php | + BaseTest.php Hinweis: Namen in eckigen Klammern bezeichnen Verzeichnisse
Refactoring: eine gemeinsame Ausgabeklasse erstellen
Wie Sie vielleicht schon bemerkt haben, teilen sich die Ausgabeklassen unserer Content-Module, HelloPageBase und HelloPlanetRatingBoxBase, einige Methoden wie get/setPluginloaderObject( ), get/setConnectorObject( ) und Funktionalität wie die Übergabe von Konfigurationsdaten und Anfrageparametern. Wir können diese doppelten Implementierungen leicht loswerden, indem wir eine gemeinsame Elternklasse erstellen, die sie alle implementiert. Da wir nicht vorhaben, diese Basisklasse zu instanziieren, sollte sie abstrakt sein. Die Klasse selbst erweitert PapayaObject, um den Zugriff auf die papaya-Application-Registry zu ermöglichen.
Anforderungen an die Ausgabeklasse
Die neue Ausgabeklasse, HelloOutput, wird folgende Attribute enthalten:
- $_pluginloaderObject, die Instanz von base_pluginloader zum Laden der Connector-Instanz
- $_connectorObject, die Instanz von HelloConnector
- $_owner, die Instanz der Eigentümerklasse, d.h. des Content-Moduls
- $_data, die Konfigurationsdaten des Content-Moduls (die öffentliche Eigenschaft $data des Eigentümers)
- $_params, die Anfrage-Parameter (die öffentliche Eigenschaft $params des Eigentümers)
Beachten Sie, dass all diese Attribute protected statt private sein müssen, weil die Ausgabeklassen der Content-Module diese Klasse erweitern und Zugriff auf diese Eigenschaften benötigen.
Wir werden die folgenden Methoden definieren:
- setPluginloaderObject($pluginloaderObject) setzt die base_pluginloader-Instanz von außen, besonders für Tests
- getPluginloaderObject() instanziiert das base_pluginloader-Objekt, falls es noch nicht definiert ist, und gibt es in jedem Fall zurück
- setConnectorObject($connectorObject) setzt die HelloConnector-Instanz von außen, besonders für Tests
- getConnectorObject() instanziiert das HelloConnector-Objekt, falls es noch nicht definiert ist, und gibt es in jedem Fall zurück
- setOwner($owner) setzt das Eigentümer-Objekt (es gibt keine getOwner( )-Methode, da dieses Objekt stets von außen gesetzt wird, entweder durch das Content-Modul oder durch den Unit-Test
- setData($data) setzt das Konfigurationsdaten-Array
- setParams($params) setzt das Array mit Anfrage-Parametern
Den Unit-Test schreiben
Wie üblich schreiben wir den Unit-Test, bevor wir die eigentliche Klasse implementieren. Hier die komplette Unit-Test-Klasse, Hello/Output.php:
<?php require_once(substr(dirname(__FILE__), 0, -52).'/Framework/PapayaTestCase.php'); PapayaTestCase::registerPapayaAutoloader(); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Output.php'); class HelloOutputTest extends PapayaTestCase { /** * @covers HelloOutput::setPluginloaderObject */ public function testSetPluginloaderObject() { $output = new HelloOutput_TestProxy(); $pluginloaderObject = $this->getMock('base_pluginloader'); $output->setPluginloaderObject($pluginloaderObject); $this->assertAttributeSame($pluginloaderObject, '_pluginloaderObject', $output); } /** * @covers HelloOutput::getPluginloaderObject */ public function testGetPluginloaderObject() { $output = new HelloOutput_TestProxy(); $pluginloaderObject = $output->getPluginloaderObject(); $this->assertInstanceOf('base_pluginloader', $pluginloaderObject); } /** * @covers HelloOutput::setConnectorObject */ public function testSetConnectorObject() { $output = new HelloOutput_TestProxy(); $connectorObject = $this->getMock('HelloConnector'); $output->setConnectorObject($connectorObject); $this->assertAttributeSame($connectorObject, '_connectorObject', $output); } /** * @covers HelloOutput::getConnectorObject */ public function testGetConnectorObject() { $output = new HelloOutput_TestProxy(); $pluginloaderObject = $this->getMock('base_pluginloader'); $connectorObject = $this->getMock('HelloConnector'); $pluginloaderObject ->expects($this->once()) ->method('getPluginInstance') ->will($this->returnValue($connectorObject)); $output->setPluginloaderObject($pluginloaderObject); $this->assertSame($connectorObject, $output->getConnectorObject()); } /** * @covers HelloOutput::setOwner */ public function testSetOwner() { $output = new HelloOutput_TestProxy(); $owner = $this->getMock( 'HelloPlanetRatingBox', array(), array(), 'Mock_'.md5(__CLASS__. microtime()), FALSE ); $output->setOwner($owner); $this->assertAttributeSame($owner, '_owner', $output); } /** * @covers HelloOutput::setData */ public function testSetData() { $output = new HelloOutput_TestProxy(); $expectedData = array('title' => 'Planet ratings'); $output->setData($expectedData); $this->assertAttributeEquals($expectedData, '_data', $output); } /** * @covers HelloOutput::setParams */ public function testSetParams() { $output = new HelloOutput_TestProxy(); $expectedParams = array('planet_id' => 1, 'rating_points' => 5); $output->setParams($expectedParams); $this->assertAttributeEquals($expectedParams, '_params', $output); } } class HelloOutput_TestProxy extends HelloOutput { // Noting here, just allow to instantiate the abstract class. }
Alle diese tests wurden bereits in früheren Teilen dieses Tutorials erläutert, da sie nur in diese neue Klasse verschoben wurden.
Einmal mehr verwenden wir eine Proxy-Klasse für die Tests. Diesmal liegt es daran, dass die Originalklasse HelloOutput abstrakt ist und nicht instanziiert werden kann.
Die Ausgabeklasse implementieren
Die Ausgabeklasse selbst, Hello/Output.php, sieht so aus:
<?php /** * Hello World tutorial, common output functionality * * Provides common functionality for content modules' output classes. * * @package Papaya-Modules * @subpackage tutorial */ /** * Hello World tutorial, common output class * * @package Papaya-Modules * @subpackage tutorial */ abstract class HelloOutput extends PapayaObject { /** * The plugin loader object to be used * @var base_pluginloader */ protected $_pluginloaderObject = NULL; /** * The HelloConnector object to be used * @var HelloConnector */ protected $_connectorObject = NULL; /** * Owner object * @var HelloPlanetRatingBox */ protected $_owner = NULL; /** * Box configuration data * @var array */ protected $_data = array(); /** * Box request parameters * @var array */ protected $_params = array(); /** * Set the plugin loader object to be used * * @param base_pluginloader $pluginloaderObject */ public function setPluginloaderObject($pluginloaderObject) { $this->_pluginloaderObject = $pluginloaderObject; } /** * Get (and, if necessary, initialize) the plugin loader object * * @return base_pluginloader */ public function getPluginloaderObject() { if (!is_object($this->_pluginloaderObject)) { include_once(PAPAYA_INCLUDE_PATH.'system/base_pluginloader.php'); $this->_pluginloaderObject = new base_pluginloader(); } return $this->_pluginloaderObject; } /** * Set the HelloConnector object to be used * * @param HelloConnector $connectorObject */ public function setConnectorObject($connectorObject) { $this->_connectorObject = $connectorObject; } /** * Get (and, if necessary, initialize) the HelloConnector object * * @return HelloConnector */ public function getConnectorObject() { if (!is_object($this->_connectorObject)) { $pluginloaderObject = $this->getPluginloaderObject(); $this->_connectorObject = $pluginloaderObject->getPluginInstance('eeb42aad2491cd607c7c64bc57eae455', $this); } return $this->_connectorObject; } /** * Set the owner object * * @param HelloPlanetRatingBox $owner */ public function setOwner($owner) { $this->_owner = $owner; } /** * Set configuration data * * @param array $data */ public function setData($data) { $this->_data = $data; } /** * Set box request parameters * @param array $params */ public function setParams($params) { $this->_params = $params; } }
Refactoring der Modul-Basisklassen
Nachdem die neue Ausgabeklasse fertig ist, kann das Refactoring der Klassen HelloPageBase und HelloPlanetRatingBoxBase stattfinden. Das bedeutet effektiv, dass Sie die Klausel extends HelloOutput hinzufügen und alle Attribut-Deklarationen und Methoden entfernen müssen, die in die neue Klasse verschoben wurden. Dasselbe gilt für die Unit-Tests. Im Fall von HelloPageBase und der zugehörigen Eigentümerklasse HelloPage müssen Sie auch zwei Methodennamen in Aufrufen ändern:
- setPageData( ) heißt nun setData( )
- setHelloConnectorObject( ) heißt nun setConnectorObject( )
Hier der überarbeitete Unit-Test für HelloPageBase, Hello/Page/BaseTest.php:
<?php require_once(substr(dirname(__FILE__), 0, -57).'/Framework/PapayaTestCase.php'); PapayaTestCase::registerPapayaAutoloader(); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Page/Base.php'); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Connector.php'); class HelloPageBaseTest extends PapayaTestCase { /** * Instantiate the HelloPageBase object to be tested * * @return HelloPageBase */ private function getHelloPageBaseObjectFixture() { $this->defineConstantDefaults('PAPAYA_DB_TBL_MODULES'); return new HelloPageBase(); } /** * Get an owner mock object (it doesn't need a specific class) * * @return object */ private function getOwnerObjectFixture() { return $this->getMock('GenericContentClass'); } /** * Get a HelloConnector mock object * * @return HelloConnector mock object */ private function getHelloConnectorObjectFixture() { return $this->getMock( 'HelloConnector', array(), array(), 'Mock_'.md5(__CLASS__.microtime()), FALSE ); } /** * @covers HelloPageBase::getPageXml */ public function testGetPageXml() { $helloPageBaseObject = $this->getHelloPageBaseObjectFixture(); $helloConnectorObject = $this->getHelloConnectorObjectFixture(); $helloConnectorObject ->expects($this->once()) ->method('getPlanetById') ->will($this->returnValue(array('planet_id' => 1, 'planet_name' => 'Mars'))); $helloPageBaseObject->setConnectorObject($helloConnectorObject); $helloPageBaseObject->setData(array('planet_id' => 1, 'text' => 'Hello')); $xml = '<title>Hello Mars!</title> <text>Hello</text>'; $this->assertEquals($xml, $helloPageBaseObject->getPageXml()); } /** * @covers HelloPageBase::getPlanetSelector */ public function testGetPlanetSelector() { $helloPageBaseObject = $this->getHelloPageBaseObjectFixture(); $owner = $this->getOwnerObjectFixture(); $owner->paramName = 'tut'; $helloConnectorObject = $this->getHelloConnectorObjectFixture(); $helloConnectorObject ->expects($this->once()) ->method('getAllPlanets') ->will($this->returnValue(array(1 => 'Mars', 2 => 'Jupiter'))); $helloPageBaseObject->setOwner($owner); $helloPageBaseObject->setConnectorObject($helloConnectorObject); $expectedXml = '<select name="tut[planet_id]" class="dialogSelect dialogScale"> <option value="1" selected="selected">Mars</option> <option value="2">Jupiter</option> </select>'; $this->assertEquals( $expectedXml, $helloPageBaseObject->getPlanetSelector('planet_id', 1) ); } }
Die Klasse selbet, Hello/Page/Base.php, sieht nun so aus:
<?php /** * Hello World tutorial page module, base class * * @package Papaya-Modules * @subpackage tutorial */ /** * Base class HelloOutput */ require_once(dirname(__FILE__).'/../Output.php'); /** * Hello World tutorial page module class, base class * * @package Papaya-Modules * @subpackage tutorial */ class HelloPageBase extends HelloOutput { /** * Get the page's XML output * * @return string XML */ public function getPageXml() { $helloConnectorObject = $this->getConnectorObject(); $planet = ''; $planetData = $helloConnectorObject->getPlanetById($this->_data['planet_id']); if (is_array($planetData) && !empty($planetData)) { $planet = $planetData['planet_name']; } $result = sprintf('<title>Hello %s!</title>'.LF, $planet); $result .= sprintf('<text>%s</text>', papaya_strings::escapeHTMLChars($this->_data['text'])); return $result; } /** * Get a select box for planets * * @param string $name * @param integer $data */ public function getPlanetSelector($name, $data) { $result = ''; $helloConnectorObject = $this->getConnectorObject(); $planetList = $helloConnectorObject->getAllPlanets(); if (is_array($planetList) && !empty($planetList)) { $result = sprintf( '<select name="%s[%s]" class="dialogSelect dialogScale">'.LF, $this->_owner->paramName, $name ); foreach ($planetList as $planetId => $planetName) { $selected = ($planetId == $data) ? ' selected="selected"' : ''; $result .= sprintf( '<option value="%d"%s>%s</option>'.LF, $planetId, $selected, $planetName ); } $result .= '</select>'; } return $result; } }
Und hier die modifizierte Hello/Page.php:
<?php /** * Hello World tutorial page module * * @package Papaya-Modules * @subpackage tutorial */ /** * Base class base_content */ require_once(PAPAYA_INCLUDE_PATH.'system/base_content.php'); /** * Hello World tutorial page module class * * @package Papaya-Modules * @subpackage tutorial */ class HelloPage extends base_content { /** * Instance of the HelloPageBase class * @var HelloPageBase */ private $_baseObject = NULL; /** * Parameter namespace * @var string */ public $paramName = 'tut'; /** * Edit fields for page configuration * @var array */ public $editFields = array( 'text' => array( 'Text', 'isNoHTML', TRUE, 'textarea', 5, '', 'Greetings from the new module' ), 'planet_id' => array( 'Planet', 'isNum', FALSE, 'function', 'callbackPlanetSelector' ) ); /** * Set the HelloPageBase object to be used * * @param HelloPageBase $baseObject */ public function setBaseObject($baseObject) { $this->_baseObject = $baseObject; } /** * Get (and, if necessary, initialize) the HelloPageBase object * * @return HelloPageBase */ public function getBaseObject() { if (!is_object($this->_baseObject)) { include_once(dirname(__FILE__).'/Page/Base.php'); $this->_baseObject = new HelloPageBase(); } return $this->_baseObject; } /** * Get the page output XML * * @return string XML */ public function getParsedData() { $this->setDefaultData(); $baseObject = $this->getBaseObject(); $baseObject->setData($this->data); return $baseObject->getPageXml(); } /** * Callback method for a planet select field * * @param string $name * @param array $field * @param integer $data * @return string XML */ public function callbackPlanetSelector($name, $field, $data) { $baseObject = $this->getBaseObject(); $baseObject->setOwner($this); return $baseObject->getPlanetSelector($name, $data); } }
Die Datei Hello/Planet/Rating/Box/BaseTest.php im Testverzeichnis sieht nach dem Entfernen einiger Test folgendermaßen aus:
<?php require_once(substr(dirname(__FILE__), 0, -70).'/Framework/PapayaTestCase.php'); PapayaTestCase::registerPapayaAutoloader(); PapayaTestCase::defineConstantDefaults( array( 'PAPAYA_DB_TBL_SURFER', 'PAPAYA_DB_TBL_SURFERGROUPS', 'PAPAYA_DB_TBL_SURFERPERM', 'PAPAYA_DB_TBL_SURFERACTIVITY', 'PAPAYA_DB_TBL_SURFERPERMLINK', 'PAPAYA_DB_TBL_SURFERCHANGEREQUESTS', 'PAPAYA_DB_TBL_TOPICS' ) ); require_once( PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Planet/Rating/Box/Base.php' ); require_once( PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Planet/Rating/Box.php' ); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Connector.php'); class HelloPlanetRatingBoxBaseTest extends PapayaTestCase { /** * @covers HelloPlanetRatingBoxBase::setDialogObject */ public function testSetDialogObject() { $baseObject = new HelloPlanetRatingBoxBase(); $dialogObject = $this->getMock( 'base_dialog', array(), array(), 'Mock_'.md5(__CLASS__. microtime()), FALSE ); $baseObject->setDialogObject($dialogObject); $this->assertAttributeSame($dialogObject, '_dialogObject', $baseObject); } /** * @covers HelloPlanetRatingBoxBase::getDialogObject */ public function testGetDialogObject() { $this->markTestSkipped(); } /** * @covers HelloPlanetRatingBoxBase::saveRatings */ public function testSaveRatingsSuccess() { $baseObject = new HelloPlanetRatingBoxBase(); $helloConnectorObject = $this->getMock('HelloConnector'); $helloConnectorObject ->expects($this->once()) ->method('modifyPlanetRatings') ->will($this->returnValue(TRUE)); $baseObject->setConnectorObject($helloConnectorObject); $baseObject->setData(array('message_success' => 'Success')); $baseObject->setParams(array('planet_1' => 4, 'planet_2' => 5)); $expectedXml = '<message type="success">Success</message> '; $this->assertEquals($expectedXml, $baseObject->saveRatings('abc123')); } /** * @covers HelloPlanetRatingBoxBase::saveRatings */ public function testSaveRatingsFailure() { $baseObject = new HelloPlanetRatingBoxBase(); $helloConnectorObject = $this->getMock('HelloConnector'); $helloConnectorObject ->expects($this->once()) ->method('modifyPlanetRatings') ->will($this->returnValue(FALSE)); $baseObject->setConnectorObject($helloConnectorObject); $baseObject->setData(array('message_error' => 'Error')); $baseObject->setParams(array('planet_1' => 4, 'planet_2' => 5)); $expectedXml = '<message type="error">Error</message> '; $this->assertEquals($expectedXml, $baseObject->saveRatings('abc123')); } /** * @covers HelloPlanetRatingBoxBase::getRatingsForm */ public function testGetRatingsForm() { $baseObject = new HelloPlanetRatingBoxBase(); $helloConnectorObject = $this->getMock('HelloConnector'); $helloConnectorObject ->expects($this->once()) ->method('getAllPlanets') ->will($this->returnValue(array(1 => 'Mars', 2 => 'Jupiter'))); $helloConnectorObject ->expects($this->once()) ->method('getUserPlanetRatings') ->will($this->returnValue(array(1 => 4, 2 => 5))); $baseObject->setConnectorObject($helloConnectorObject); $dialogObject = $this->getMock( 'base_dialog', array(), array(), 'Mock_'.md5(__CLASS__. microtime()), FALSE ); $dialogObject ->expects($this->once()) ->method('getDialogXML') ->will($this->returnValue('<dialog />')); $baseObject->setDialogObject($dialogObject); $baseObject->setData(array('caption_dialog' => 'Rate planets', 'caption_submit' => 'Rate')); $this->assertEquals('<dialog />', $baseObject->getRatingsForm('abc123')); } /** * @covers HelloPlanetRatingBoxBase::getRatingsXml */ public function testGetRatingsXml() { $baseObject = new HelloPlanetRatingBoxBase(); $expectedXml = '<ratings> <planet id="1" name="Mars" rating="5.0" /> <planet id="2" name="Jupiter" rating="4.5" /> </ratings> '; $connectorObject = $this->getMock('HelloConnector'); $connectorObject ->expects($this->once()) ->method('getPlanetRatings') ->will( $this->returnValue( array( 1 => array('planet_id' => 1, 'planet_name' => 'Mars', 'points' => 5), 2 => array('planet_id' => 2, 'planet_name' => 'Jupiter', 'points' => 4.5) ) ) ); $baseObject->setConnectorObject($connectorObject); $this->assertEquals($expectedXml, $baseObject->getRatingsXml()); } /** * @covers HelloPlanetRatingBoxBase::getBoxXml */ public function testGetBoxXmlLoggedInSave() { $baseObject = new HelloPlanetRatingBoxBase(); $connectorObject = $this->getMock('HelloConnector'); $connectorObject ->expects($this->once()) ->method('modifyPlanetRatings') ->will($this->returnValue(TRUE)); $connectorObject ->expects($this->once()) ->method('getPlanetRatings') ->will($this->returnValue(array())); $baseObject->setConnectorObject($connectorObject); $surfer = $this->getMock('base_surfer'); $surfer->isValid = TRUE; $surfer->surferId = 'abc123'; $application = $this->getMockApplicationObject(array('surfer' => $surfer)); $baseObject->setApplication($application); $baseObject->setData(array('title' => 'Planet ratings', 'message_success' => 'Success')); $baseObject->setParams(array('save' => 1, 'planet_1' => 4, 'planet_2' => 5)); $expectedXml = '<ratingbox> <title>Planet ratings</title> <message type="success">Success</message> </ratingbox> '; $this->assertEquals($expectedXml, $baseObject->getBoxXml()); } /** * @covers HelloPlanetRatingBoxBase::getBoxXml */ public function testGetBoxXmlLoggedInDialog() { $baseObject = new HelloPlanetRatingBoxBase(); $connectorObject = $this->getMock('HelloConnector'); $connectorObject ->expects($this->once()) ->method('getAllPlanets') ->will($this->returnValue(array())); $baseObject->setConnectorObject($connectorObject); $surfer = $this->getMock('base_surfer'); $surfer->isValid = TRUE; $surfer->surferId = 'abc123'; $application = $this->getMockApplicationObject(array('surfer' => $surfer)); $baseObject->setApplication($application); $baseObject->setData(array('title' => 'Planet ratings')); $expectedXml = '<ratingbox> <title>Planet ratings</title> </ratingbox> '; $this->assertEquals($expectedXml, $baseObject->getBoxXml()); } /** * @covers HelloPlanetRatingBoxBase::getBoxXml */ public function testGetBoxXmlNotLoggedIn() { $baseObject = new HelloPlanetRatingBoxBase(); $connectorObject = $this->getMock('HelloConnector'); $connectorObject ->expects($this->once()) ->method('getPlanetRatings') ->will($this->returnValue(array())); $baseObject->setConnectorObject($connectorObject); $surfer = $this->getMock('base_surfer'); $surfer->isValid = FALSE; $application = $this->getMockApplicationObject(array('surfer' => $surfer)); $baseObject->setApplication($application); $baseObject->setData(array('title' => 'Planet ratings')); $expectedXml = '<ratingbox> <title>Planet ratings</title> </ratingbox> '; $this->assertEquals($expectedXml, $baseObject->getBoxXml()); } }
Und hier die Datei Hello/Page/Rating/Box/Base.php im Modulverzeichnis:
<?php /** * Hello World tutorial, Planet rating box base class * * Provides basic functionality for the rating box. * * @package Papaya-Modules * @subpackage tutorial */ /** * Base class HelloOutput */ require_once(dirname(__FILE__).'/../../../Output.php'); /** * Hello World tutorial, Planet rating box base class * * @package Papaya-Modules * @subpackage tutorial */ class HelloPlanetRatingBoxBase extends HelloOutput { /** * The dialog object to be used * @var base_dialog */ private $_dialogObject = NULL; /** * Set the dialog object to be used * * @param base_dialog $dialogObject */ public function setDialogObject($dialogObject) { $this->_dialogObject = $dialogObject; } /** * Get (and, if necessary, initialize) the dialog object * * @param array $fields * @param array $data * @param array $hidden * @return base_dialog */ public function getDialogObject($fields, $data, $hidden) { if (!is_object($this->_dialogObject)) { $this->_dialogObject = new base_dialog( $this, $this->_owner->paramName, $fields, $data, $hidden ); } return $this->_dialogObject; } /** * Save the ratings * * @param string $surferId * @return string XML */ public function saveRatings($surferId) { $ratings = array(); $result = ''; foreach ($this->_params as $field => $value) { if (strpos($field, 'planet_') === 0) { $id = substr($field, 7); $ratings[$id] = $value; } } if (!empty($ratings)) { $connectorObject = $this->getConnectorObject(); $success = $connectorObject->modifyPlanetRatings($surferId, $ratings); if ($success) { $result = sprintf( '<message type="success">%s</message>'.LF, papaya_strings::escapeHTMLChars($this->_data['message_success']) ); } else { $result = sprintf( '<message type="error">%s</message>'.LF, papaya_strings::escapeHTMLChars($this->_data['message_error']) ); } } return $result; } /** * Get the ratings form * * @param string $surferId * @return string XML */ public function getRatingsForm($surferId) { $result = ''; $connectorObject = $this->getConnectorObject(); $planets = $connectorObject->getAllPlanets(); if (!empty($planets)) { $ratingCombo = array(1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5); $fields = array(); foreach ($planets as $id => $name) { $fields['planet_'.$id] = array($name, 'isNum', FALSE, 'combo', $ratingCombo); } $ratings = $connectorObject->getUserPlanetRatings($surferId); $data = array(); if (!empty($ratings)) { foreach ($ratings as $id => $points) { $data['planet_'.$id] = $points; } } $hidden = array('save' => 1); $dialog = $this->getDialogObject($fields, $data, $hidden); if (is_object($dialog)) { $dialog->dialogTitle = $this->_data['caption_dialog']; $dialog->buttonTitle = $this->_data['caption_submit']; $result = $dialog->getDialogXML(); } } return $result; } /** * Get XML for the average ratings */ public function getRatingsXml() { $result = ''; $connectorObject = $this->getConnectorObject(); $ratings = $connectorObject->getPlanetRatings(); if (!empty($ratings)) { $result = '<ratings>'.LF; foreach ($ratings as $id => $data) { $result .= sprintf( '<planet id="%d" name="%s" rating="%.1f" />'.LF, $id, papaya_strings::escapeHTMLChars($data['planet_name']), $data['points'] ); } $result .= '</ratings>'.LF; } return $result; } /** * Get box XML output * * @return string XML */ public function getBoxXml() { $result = '<ratingbox>'.LF; $result .= sprintf( '<title>%s</title>'.LF, papaya_strings::escapeHTMLChars($this->_data['title']) ); $surfer = $this->getApplication()->surfer; if ($surfer->isValid) { if (isset($this->_params['save']) && $this->_params['save'] == 1) { $result .= $this->saveRatings($surfer->surferId); $result .= $this->getRatingsXml(); } else { $result .= $this->getRatingsForm($surfer->surferId); } } else { $result .= $this->getRatingsXml(); } $result .= '</ratingbox>'.LF; return $result; } }
Test-Suite-Dateien hinzufügen
Um sicherzustellen, dass alle Unit-Tests ausgeführt werden, können Sie zu jedem Unterverzeichnis Ihres Test-Verzeichnisses eine sogenannte Test-Suite-Datei hinzufügen. Diese Dateien heißen üblicherweise AllTests.php. Jede von ihnen definert eine Klasse namens <Verzeichnisname>_AllTests ohne bestimmte Basisklasse. Fügen Sie require_once-Anweisungen für die AllTests.php-Datei in jedem direkten Unterverzeichnis und für jede Testdatei im aktuellen Verzeichnis hinzu. Fügen Sie in der Klasse eine öffentliche, statische Methode namens suite( ) hinzu. Sie definiert eine Instanz von PHPUnit_Framework_TestSuite mit einem Beschreibungs-String als Argument. Rufen Sie die Methode addSuite( ) dieser Instanz mit dem Klassennamen jeder inkludierten Testdatei als String auf. Geben Sie die Instanz zurück, und Sie sind fertig.
Beginnen Sie in den innersten Verteichnissen, die keine weiteren Unterverzeichnisse mehr enthalten, mit dem Schreiben der AllTests-Klassen. Hier ist beispielsweise der Inhalt der Datei Hello/Page/AllTests.php, die nur die Testdatei BaseTest.php inkludiert:
<?php require_once(dirname(__FILE__).'/BaseTest.php'); class HelloPage_AllTests { public static function suite() { $suite = new PHPUnit_Framework_TestSuite('HelloPage all tests'); $suite->addTestSuite('HelloPageBaseTest'); return $suite; } }
Arbeiten Sie sich den Verzeichnisbaum entlang nach oben. Die letzte Test-Suite, die Sie schreiben, ist Hello/AllTests.php; sie sieht so aus:
<?php require_once(dirname(__FILE__).'/Page/AllTests.php'); require_once(dirname(__FILE__).'/Planet/AllTests.php'); require_once(dirname(__FILE__).'/ConnectorTest.php'); require_once(dirname(__FILE__).'/OutputTest.php'); require_once(dirname(__FILE__).'/PageTest.php'); require_once(dirname(__FILE__).'/PlanetTest.php'); class Hello_AllTests { public static function suite() { $suite = new PHPUnit_Framework_TestSuite('Hello all tests'); $suite->addTestSuite('HelloPage_AllTests'); $suite->addTestSuite('HelloPlanet_AllTests'); $suite->addTestSuite('HelloConnectorTest'); $suite->addTestSuite('HelloOutputTest'); $suite->addTestSuite('HelloPageTest'); $suite->addTestSuite('HelloPlanetTest'); return $suite; } }
Wenn Sie fertig sind, führen Sie diese Datei auf der Konsole aus:
$ phpunit AllTests.php
Wenn Sie nicht sicher sind, ob Sie alle Tests in die verschachtelten Test-Suites eingefügt haben, vergleichen Sie die Ausgabe von AllTests.php mit derjenigen eines einfachen phpunit . im selben Verzeichnis. Auch wenn die Ausführungsreihenfolge anders ist, muss die Anzahl der Tests identisch sein.
Hinweis: Wie Sie sehen, kann phpunit selbst rekursiv Unterverzeichnisse abarbeiten, ohne Test-Suites zu verwenden. Dennoch sollten Sie Suites verwenden, weil Sie Ihre eigene Ausführungsreihenfolge definieren oder einige noch nicht fertige Tests ausschließen können. Außerdem basieren Continuous-Integration-Systeme wie PHPUnderControl auf Test-Suites.
Die Code-Coverage messen
Nachdem Sie mit dem Schreiben der Test-Suites fertig sind, können Sie sie verwenden, um die Code-Coverage zu messen, d.h. den Prozentsatz des Codes, der durch Unit-Tests abgedeckt ist. Rufen Sie dazu die oberste AllTests-Suite auf der Konsole auf und fügen Sie die Option --coverage-html <Pfad> hinzu (der Pfad ist das Verzeichnis, in dem PHPUnit den Coverage-Bericht speichern soll; wenn es noch nicht existiert, wird es erzeugt). Beispiel:
$ phpunit --coverage-html /home/username/coverage AllTests.php
Das Erzeugen des Coverage-Berichts dauert ein wenig länger. Wenn er fertig ist, können Sie die Datei index.html aus dem Coverage-Verzeichnis in Ihrem Web-Browser öffnen. Von dort aus können Sie die verschiedenen Verzeichnisse und Dateien durchblättern.