Module Development 5: Refactoring and Miscellania

From PapayaCMS

Jump to: navigation, search

Tutorial

Abstract This tutorial describes how to get rid of double implementations by refactoring and how to provide test suites.
Intended audience PHP developers
Level intermediate
Software requirements SVN checkout of papaya CMS; instructions here
PHPUnit >= 3.5 for unit tests
Date 2011-04-19
Previous tutorial Module Development 4: Adding Frontend Interactivity

In this final part of the tutorial series, we will do some refactoring to get rid of double implementations. After everything is in its place, we will add test suite files to control execution of all unit tests and to measure code coverage.

Please note that you should adhere to the Papaya CMS Coding Standards, especially if you plan to contribute your modules for the papaya Community.

This tutorial makes use of unit tests which is a highly recommended way to build software. In order to have a papaya CMS version that includes the PHPUnit framework and the PapayaTestCase class, you need to check out papaya CMS from the SVN repository. Instructions on how to obtain it can be found here.

Contents

Overview of files and directories

After finishing this tutorial, you will have created the following files and subdirectories in papaya-lib/modules/special/myproject/tutorial (new files from the current part are bold):

+ [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

 Note: Names in square brackets indicate directories

In the testing/tests-unittests/papaya-lib/modules/special/myproject/tutorial directory, you will have created the following structure (new files from the current part are bold):

+ 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

 Note: Names in square brackets indicate directories

Refactoring: Creating a common output class

As you may have noticed, the output classes of our content modules, HelloPageBase and HelloPlanetRatingBoxBase, share a few methods like get/setPluginloaderObject( ), get/setConnectorObject( ), and functionality like passing configuration data and request paremeters. We can easily get rid of these double implementations by defining a common parent class that implements all of them. As we never attend to instantiate this base class, it should be abstract. The class itself extends PapayaObject to allow access to the papaya application registry.

Requirements of the output class

The new output class, HelloOutput, will contain the following attributes:

  • $_pluginloaderObject, the instance of base_pluginloader to load the connector instance
  • $_connectorObject, the instance of HelloConnector
  • $_owner, the instance of the owner class, i.e. the content module
  • $_data, the content module's configuration data (the owner's public $data property)
  • $_params, the request parameters (the owner's public $params property)

Note that all of these attributes must be protected rather than private because the content modules' output classes extend this class and need access to these properties.

We are going to define the following methods:

  • setPluginloaderObject($pluginloaderObject) sets the base_pluginloader instance from outside, especially for testing
  • getPluginloaderObject() instantiates the base_pluginloader object if it's not defined yet and returns it in any case
  • setConnectorObject($connectorObject) sets the HelloConnector instance from outside, especially for testing
  • getConnectorObject() instantiates the HelloConnector object if it's not defined yet and returns it in any case
  • setOwner($owner) sets the owner object (there is no getOwner( ) method as this object is always set from outside, either from the content module or from the unit test
  • setData($data) sets the configuration data array
  • setParams($params) sets the array of request parameters

Writing the unit test

As usual, we write the unit test before we implement the real class. Here's the complete unit test class, 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.
}

All of these tests have already been described in previous parts of this tutorial as they have only been moved to this new class.

Once more, we use a proxy class for the tests. This time it's because the original HelloOutput class is abstract and cannot be instantiated.

Implementing the output class

The output class itself, Hello/Output.php, looks like this:

<?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 the module base classes

Now that the new output class is ready, the HelloPageBase and HelloPlanetRatingBoxBase classes can be refactored. This means effectively that you need to add the extends HelloOutput clause and remove all attribute declarations and methods that have been moved to the new class. It's the same for the unit tests. In case of HelloPageBase and its owner class HelloPage, you also need to change two method names in invocations:

  • setPageData( ) is now setData( )
  • setHelloConnectorObject( ) is now setConnectorObject( )

Here's the revised unit test for 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)
    );
  }
}

The class file itself, Hello/Page/Base.php, looks like this now:

<?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;
  }
}

And here's the modified 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);
  }
}

The Hello/Planet/Rating/Box/BaseTest.php file in the test directory reads as follows after removing some tests:

<?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());
  }
}

And here's the Hello/Page/Rating/Box/Base.php file in the modules directory:

<?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;
  }
}

Adding test suite files

In order to make sure that all unit tests are executed, you can add a so-called test suite file to each subdirectory in your test directory. These files are usually called AllTests.php. Each of these files defines a class called <DirectoryName>_AllTests without a specific base class. Add require_once statements for the AllTests.php file in each direct subdirectory and for each test file in the current directory. In the class, add a public static method called suite( ). It defines an instance of PHPUnit_Framework_TestSuite with a descriptive string as an argument. Call this instance's addSuite( ) method with the class name of each test suite you included as a string. Return the instance, and you're done.

Start writing AllTests classes in the innermost directories that do not contain any further subdirectories. For example, here's the contents of Hello/Page/AllTests.php that only includes the BaseTest.php test file:

<?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;
  }
}

Work your way up the directory tree. The last test suite you write is Hello/AllTests.php which looks like this:

<?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;
  }
}

When you're done, run this file on the console:

$ phpunit AllTests.php

If you're not sure whether you included all of your tests into the nested test suites, compare the output of AllTests.php to that of a simple phpunit . in the same directory. Although the execution order will be different, the number of tests must be the same.

Note: As you can see, phpunit itself can recurse into subdirectories without using test suites. You might want to use suites, though, because you can define your own execution order or exclude some tests that are not ready yet. Besides, continuous integration systems like PHPUnderControl rely on test suites.

Measuring code coverage

Once you're done writing test suites, you can use them to measure the code coverage, i.e. the percentage of code covered by unit tests. To do this, invoke the top-level AllTests suite on the console, adding a --coverage-html <path> option (the path is the directory in which you want PHPUnit to store the coverage report; if it does not exist, it will be created). Example:

$ phpunit --coverage-html /home/username/coverage AllTests.php

Generating the coverage report will consume some extra time. When it's ready, you can open the index.html file from the coverage directory in your web browser. From there, you can browse the different directories and files.

Personal tools
In other languages