Testing MVC Actions classes

In developing web applications with a Model-View-Controller “flavor”, I find that in the Controller portion of the architecture I invariably end up with “Action” classes. These are implemented as a Strategy pattern (i.e. a family of algorithms, encapsulated as classes with the same method signature, making them interchangeable). In my code, I end up with something like:

<?php
class Action {
  function Action() {}
  function Perform() {}
}
?>

And there might be some kind of an application controller, e.g.:

<?php
switch (strtolower($_REQUEST['action'])) {
case 'dosomething':
  $action = 'DoSomething';  break;
default:
  $action = 'DefaultAction'; 
}
if (!class_exists($action)) {
  require_once ACTION_INCLUDE_DIR.$action.'.php';
}
$action_instance =& new $action;
$action_instance->perform();
?>

Now inside these classes they usually need to access various model classes which in the past I had done by simply creating an instance of the desired model as a local variable in the Perform() method. This however, makes the code very hard to unit test.

The solution I have come up with is this; optionally pass in the model classes in the constructor for each action. The problem is that in order for the Actions to be members of the Strategy Pattern, they need to keep the same public method signature, including the constructors. So…the models need to be optional. Here is an example of the kind of code I ended up with:

<?php
require_once MODEL_INCLUDE_DIR.'SomeModel.php';
require_once MODEL_INCLUDE_DIR.'UserMessageStack.php';
class DoSomething {
  /**#@+
   *  @private
   */
  var $_model;
  var $_log;
  /**#@-*/
  /**
   * constructor
   * @return void
   */
  function InitRfpStage($model=false, $log=false) {
    if (is_object($model_instance =& obj_from_envelope($model))) {
      $this->_model =& $model_instance;
    } else {
      $this->_model =& new SomeModel;
    }
    if (is_object($log_instance =& obj_from_envelope($log))) {
      $this->_log =& $log_instance;
    } else {
      $this->_log =& new UserMessageStack;
    }
  }
  /**
    * do the action
    * @return void
    */
  function Perform() {
  }
}
?>

And the secret to having optional objects by reference in PHP4 is—the obj_from_envelope() function:

<?php
/**
 *  extract a reference to an object from array[0] or return false
 *  @param  array   $paEnvelope   an array(&$object)
 *  @return object
 */
function &obj_from_envelope($paEnvelope)
{
  if (is_array($paEnvelope)
      && array_key_exists(0, $paEnvelope)
      && is_object($paEnvelope[0])) {
    $o =& $paEnvelope[0];
    return $o;
  }
  return false;
}
?>

In a test case, you can do something like:

<?php
function TestPerform() {
  $model =& new MockSomeModel($this);
  //set up some expectations 

  $action =& new SomeAction;
  $action->Perform(array(& $model));

  $model->tally();
}
?>

So what you end up with as an end result is that under normal circumstance, the actions will be instantiated with no parameters, and will simply create the model classes. Under Unit Testing however, you can selectively choose to substitute in mock objects to better test the code under boundary conditions, etc.

Hope someone finds this useful :)