How to test controllers with CodeIgniter?

2019-03-15 09:43发布

问题:

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

回答1:

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().



回答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;
                }
        }


回答3:

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.



回答4:

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



回答5:

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.