views:

2461

answers:

4

I have a PHP web application built with CodeIgniter MVC framework. I wish to test various controller classes. I'm using Toast for unit testing. My controllers have no state, everything they process is either saved into session or passed to view to display. Creating a mock session object and testing whether that works properly is straightforward (just create a mock object and inject it with $controller->session = $mock).

What I don't know, is how to work with views. In CodeIgniter, views are loaded as:

$this->load->view($view_name, $vars, $return);

Since I don't want to alter CI code, I though I could create a mock Loader and replace the original. And here lies the problem, I cannot find a way to derive a new class from CI_Loader.

If I don't include the system/libraries/Loader.php file, the class CI_Loader is undefined and I cannot inherit from it:

class Loader_mock extends CI_Loader

If I do include the file (using require_once), I get the error:

Cannot redeclare class CI_Loader

Looks like CI code itself does not use require_once from whatever reason.

Does anyone here have experience with unit testing CodeIgniter powered applications?

Edit: I tried to inject a real loader object at run-time into a mock class, and redirect all calls and variables with __call, __set, __get, __isset and __unset. But, it does not seem to work (I don't get any errors though, just no output, i.e. blank page from Toast). Here's the code:

class Loader_mock 
{
    public $real_loader;
    public $varijable = array();

    public function Loader_mock($real)
    {
        $this->real_loader = $real;
    }

    public function __call($name, $arguments) 
    {
        return $this->real_loader->$name($arguments);
    }

    public function __set($name, $value)
    {
        return $this->real_loader->$name = $value;
    }

    public function __isset($name)
    {
        return isset($this->real_loader->$name);
    }

    public function __unset($name)
    {
        unset($this->loader->$name);
    }

    public function __get($name)
    {
        return $this->real_loader->$name;
    }

    public function view($view, $vars = array(), $return = FALSE)
    {
        $varijable = $vars;
    }
}
+2  A: 

My current solution is to alter the CodeIgniter code to use require_once instead of require. Here's the patch I'm going to send to CI developers in case someone needs to do the same until they accept it:

diff --git a/system/codeigniter/Common.php b/system/codeigniter/Common.php
--- a/system/codeigniter/Common.php
+++ b/system/codeigniter/Common.php
@@ -100,20 +100,20 @@ function &load_class($class, $instantiate = TRUE)
        // folder we'll load the native class from the system/libraries folder.
        if (file_exists(APPPATH.'libraries/'.config_item('subclass_prefix').$class.EXT))
        {
-               require(BASEPATH.'libraries/'.$class.EXT);
-               require(APPPATH.'libraries/'.config_item('subclass_prefix').$class.EXT);
+               require_once(BASEPATH.'libraries/'.$class.EXT);
+               require_once(APPPATH.'libraries/'.config_item('subclass_prefix').$class.EXT);
                $is_subclass = TRUE;
        }
        else
        {
                if (file_exists(APPPATH.'libraries/'.$class.EXT))
                {
-                       require(APPPATH.'libraries/'.$class.EXT);
+                       require_once(APPPATH.'libraries/'.$class.EXT);
                        $is_subclass = FALSE;
                }
                else
                {
-                       require(BASEPATH.'libraries/'.$class.EXT);
+                       require_once(BASEPATH.'libraries/'.$class.EXT);
                        $is_subclass = FALSE;
                }
        }
Milan Babuškov
+1  A: 

I can't help you much with the testing, but I can help you extend the CI library.

You can create your own MY_Loader class inside /application/libraries/MY_Loader.php.

<?php
  class MY_Loader extends CI_Loader {

    function view($view, $vars = array(), $return = FALSE) {
      echo 'My custom code goes here';
    }

  }

CodeIgniter will see this automatically. Just put in the functions you want to replace in the original library. Everything else will use the original.

For more info check out the CI manual page for creating core system classes.

dprevite
+1 for the idea. However, a golden rule of unit testing is that you shouldn't need to modify the system just to enable hooks for unit tests. Ideally, classes that are not part of unit testing suite shouldn't have any unit testing baggage with them. Also, keeping all unit testing code for a single class of tests in the same file makes it much easier to manage (and also remove unit testing code when deploying into production).
Milan Babuškov
+1  A: 

Hey Milan,

I'm impressed by the code you are trying to use.

So now I'm wondering how the 'Hooks' class of CodeIgniter could be of any help to your problem?

http://codeigniter.com/user_guide/general/hooks.html

Kind regards, Rein Groot

+1 for the tip. It seems that it could be doable, but in somewhat awkward way. I decided to patch CodeIgniter instead - it's a simple change, and I hope it will get into mainstream CI code one day (as I don't see any reasons why it shouldn't), so I won't have to patch it when new versions are released.
Milan Babuškov
+2  A: 

Alternatively, you could do this:

$CI =& get_instance();
$CI = load_class('Loader');

class MockLoader extends CI_Loader
{
    function __construct()
    {
     parent::__construct();
    }
}

Then in your controller do $this->load = new MockLoader().

Kevin
Nice idea, thanks.
Milan Babuškov