12

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;
    }
}
Charles
  • 50,943
  • 13
  • 104
  • 142
Milan Babuškov
  • 59,775
  • 49
  • 126
  • 179

5 Answers5

4

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
  • 13,044
  • 11
  • 55
  • 76
2

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
  • 59,775
  • 49
  • 126
  • 179
1

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
  • 869
  • 6
  • 9
  • 3
    +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 Jul 16 '09 at 22:53
1

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

Milan Babuškov
  • 59,775
  • 49
  • 126
  • 179
user141963
  • 11
  • 1
  • +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 Jul 21 '09 at 13:09
0

The controller should not contain domain logic, so unit tests make no sense here.

Instead I would test the controllers and views with acceptance tests.

Patrick
  • 922
  • 11
  • 22
  • Well ... while there is no domain logic in the controllers, they *are* responsible for managing user input. And altering state of model layer based on that input. That's where one would focus on in the unit tests for controllers. – tereško Nov 26 '13 at 09:17
  • I don't think the controller should alter the state of anything in the model layer. It should just receive the request and then pass it to the model layer (after handling web security etc). If you are altering entities or data then I guess you would need to test. But you should not be doing that in the first place. – Patrick Nov 26 '13 at 09:23
  • 1
    If you pass username and password to model layer, you are altering state of it. Or if you send a title of article to be saved, you are also altering state of model layer. And what you called "web security" is something that should be performed in model layer (maybe except CSRF prevention ... that might be better done when initializing `Request` instance). Though, it is possible that we are using same term for completely different [concepts](http://stackoverflow.com/a/5864000/727208). – tereško Nov 26 '13 at 19:30
  • I think I misunderstood you. I thought by altering you meant directly altering an entity from the controller. Thanks for the link. – Patrick Nov 27 '13 at 07:39