ExpressionEngine CMS
Open, Free, Amazing

Thread

This is an archived forum and the content is probably no longer relevant, but is provided here for posterity.

The active forums are here.

Improved CodeIgniter Unit Testing

September 15, 2009 11:46pm

Subscribe [15]
  • #1 / Sep 15, 2009 11:46pm

    istvanp

    11 posts

    After I checked out the Unit Testing class in CI and some other implemetations in CI of SimpleTest—such as Jamie Rumbelow’s (http://jamierumbelow.net/2009/08/11/setting-up-the-perfect-codeigniter-tdd-environment/)—I was kinda unhappy with CI’s presentation and SimpleTest’s ‘bloatedness’ so I decided to take CI’s class and improve it a little by addding some features and enhance its UI. You can see a screenshot of how it looks like below, in the attachments.

    Deprecated. Latest version is here.

    Main Features / Pros
    * No external libraries, uses CI’s Unit Test class and Benchmark class for timings
    * Allows you to run tests by category or one specific test. Possible categories are: Models, Views, Controllers, Helpers and Libraries.
    * All tests you write are automatically categorized depending on the suffix you give them (e.g. _model, _helper, etc.)
    * Gives you a summary of tests that were successful in a large bar on top, including total run time
    * No complicated directory structure, all tests are in one controller/file
    * Simple and easy implementation

    Missing Features / Cons
    * All tests being in one file might get a bit overwhelming if you make a lot of tests and if you are working with more than one developer/unit tester & working on one single file is not optimal even with revision control
    * Not overloading CI’s Unit Test class, it’s edited directly (with reason: it’s just two lines!)
    * Not yet tested thoroughly, but that’s why it’s here!
    * More? Give feedback here!

    Implementation

    1. Grab the CSS from the attachments below (unit_test.zip) and put it where it makes sense to you (I use /assets/css/unit_test.css). Make sure to set the correct path in step 4.
    2. Edit /system/libraries/Unit_test.php and locate the $result array near line 80 and add the two commented lines:

    'test_datatype' => gettype($test),
    'test_value'    => $test,          // Add this…
    'res_datatype'  => $extype,
    'res_value'     => $expected,      // and this
    'result'        => ($result === TRUE) ? 'passed' : 'failed',

    3. Edit /system/language/english/unit_test_lang.php and add these two array values:

    $lang['ut_test_value'] = 'Test Value';
    $lang['ut_res_value']  = 'Expected Value';

    4. Add the view test_unit.php (see attached zip) to your view folder (or any subfolder, just make sure to correctly specify the path in the controller in the next step)
    5. Last but not least, the controller test.php that you will place in your controller folder is included in the attached zip. The next post has the complete code pasted for convenience & analysis.

    —Post truncated, see next post (character limit exceeded)—

  • #2 / Sep 16, 2009 12:01am

    istvanp

    11 posts

    5. The controller:

    <?php if (!defined('BASEPATH')) exit('No direct script access allowed');
    
    /**
     * Test class
     *
     * @author Istvan Pusztai (twitter.com/istvanp)
     **/
     
    class Test extends Controller {
    
        var $timings = array();
        var $tests = array();
        
        function Test()
        {
            parent::Controller();
            
            // Set time marker for the start of the test suite
            $this->benchmark->mark('first');
            
            log_message('debug', 'Test Controller Initialized');
            
            // Load the unit test library
            $this->load->library('unit_test');
            
            // Load syntax highlighting helper
            $this->load->helper('text');
            
            // Set mode to strict
            $this->unit->use_strict(TRUE);
            
            // Disable database debugging so we can test all units without stopping
            // at the first SQL error
            $this->db->db_debug = FALSE;
            
            // Create list of tests
            $this->_map_tests();
        }
        
        function _remap()
        {    
            $view_data = array();
            $action = $this->uri->rsegment(2);
            $view_data['headers'] = array();
            
            switch ($action)
            {
                case 'index':
                    $view_data['msg'] = "Please pick a test suite";
                break;
                case 'all':
                    $counter = 0;
                    foreach ($this->tests as $key => $type)
                    {
                        $view_data['headers'][$counter] = $key;
                        foreach($type as $method)
                        {
                            ++$counter;
                            call_user_func(array($this, $method));
                        }
                    }
                break;
                case 'models':
                case 'views':
                case 'controllers':
                case 'libraries':
                case 'helpers':
                    if (array_key_exists($action, $this->tests) && count($this->tests[$action]) > 0)
                    {
                        foreach ($this->tests[$action] as $method)
                        {
                            call_user_func(array($this, $method));
                        }
                    }
                    else
                    {
                        $view_data['msg'] = "There are no test suites for $action";
                    }
                break;
                default:            
                    if ($this->_array_search_recursive($action, $this->tests))
                    {
                        call_user_func(array($this, $action));
                    }
                    else
                    {
                        $view_data['msg'] = "<em>$action</em> is an invalid test suite";
                    }
            }
            
            // Prepare report
            $report = $this->unit->result();
            
            // Prepare totals
            $view_data['totals']['all'] = count($report);
            $view_data['totals']['failed'] = 0;
            
            // Count failures
            foreach($report as $key => $test)
            {
                if ($test['Result'] == 'Failed')
                {
                    ++$view_data['totals']['failed'];
                }
            }
            
            // Count passes
            $view_data['totals']['passed'] = $view_data['totals']['all'] - $view_data['totals']['failed'];
            
            // Calculate the total time taken for the test suite
            $view_data['total_time'] = $this->benchmark->elapsed_time('first', 'end');
            
            // Other useful data
            $view_data['tests']     = $this->tests;
            $view_data['type']      = $action;
            $view_data['report']    = $report;
            $view_data['timings']   = $this->timings;
            
            $this->load->view('unit_test', $view_data);
        }
    
        function _map_tests()
        {
            $methods = get_class_methods($this);
            natsort($methods);
            
            foreach ($methods as $method)
            {
                if (strpos($method, '_') !== 0
                    AND $method != "CI_Base"
                    AND $method != "Controller"
                    AND $method != "Test"
                    AND $method != "get_instance"
                )
                {
                    $length = strlen($method);
                    
                    if (strripos($method, 'model') === $length - 5)
                    {
                        $this->tests['models'][] = $method;
                    }
                    else if (strripos($method, 'view')  === $length - 4)
                    {
                        $this->tests['views'][] = $method;
                    }
                    else if (strripos($method, 'controller')  === $length - 10)
                    {
                        $this->tests['controllers'][] = $method;
                    }
                    else if (strripos($method, 'library')  === $length - 7)
                    {
                        $this->tests['libraries'][] = $method;
                    }
                    else if (strripos($method, 'helper')  === $length - 6)
                    {
                        $this->tests['helpers'][] = $method;
                    }
                }
            }
            
            return $this->tests;
        }
        
        function _array_search_recursive($needle, $haystack, $strict = false, $path = array())
        {
            if ( ! is_array($haystack))
            {
                return false;
            }
         
            foreach ($haystack as $key => $val)
            {
                if (is_array($val) && $subPath = array_search_recursive($needle, $val, $strict, $path))
                {
                    $path = array_merge($path, array($key), $subPath);
                    return $path;
                }
                else if (( ! $strict && $val == $needle) || ($strict && $val === $needle))
                {
                    $path[] = $key;
                    return $path;
                }
            }
            
            return false;
        }
    }
    /* End of file test.php */

    —Example unit test function in next post—

  • #3 / Sep 16, 2009 12:01am

    istvanp

    11 posts

    Example function

    function user_model()
    {
        $model_name = 'user_model';        
        $this->load->model($model_name);
        
        // Insert
        $this->benchmark->mark('start');
        $test = $this->user_model->create('admin', 'admin', '[email protected]', 'Administrator');
        $this->unit->run($test, TRUE, $model_name . '->create');
        $id = $this->db->insert_id(); // save insert id
        $this->benchmark->mark('end');
        $this->timings[] = $this->benchmark->elapsed_time('start', 'end');
    
        // Delete
        $this->benchmark->mark('start');
        $test = $this->user_model->delete($id);
        $this->unit->run($test, TRUE, $model_name . '->delete');
        $this->benchmark->mark('end');
        $this->timings[] = $this->benchmark->elapsed_time('start', 'end');
    }

    Create more functions like these in the Test controller and they will automatically be recognized as long as you give a proper suffix (_model, _view, _controller, _library, _helper). You can access a test directly via an URL like /test/user_model/ which is also accessible via the drop down menu on top.

    REMINDER: Typically you would only want to have such code on a sandbox and/or on a production server with proper authentication. You don’t want to reveal any piece of information that might be used against your site for an attack.

    Enjoy!

  • #4 / Sep 16, 2009 12:48am

    jegbagus

    38 posts

    thanks istvanp,
    i never use codeigniter testing library before (except elapsed time),
    it would be very good library for testing my application performance…

  • #5 / Sep 16, 2009 1:00am

    istvanp

    11 posts

    @jebagus You are welcome. For testing performance without doing any extra work like here, you can try a PHP extension called Xdebug which is more suited for the job. Check their profiling section http://www.xdebug.org/docs/profiler for more details.

  • #6 / Sep 16, 2009 1:22am

    jegbagus

    38 posts

    hi istvanp, thanks for the link..
    i fast reading the documentation, and i find that this tools is really interesting.
    but can this tools implemented in Codeigniter?
    as you know CI controller / model / plugin etc, never use Calls to include, it will generate the right profiling result?
    regards

  • #7 / Sep 16, 2009 1:34am

    istvanp

    11 posts

    @jegbagus Xdebug is a PHP extension just like how MySQL is and many others… which means that you do not need to explicitly call it and it does not add extra overhead to your script (albeit it consumes more memory and writes to the disk after the fact). Do take note that you don’t get 1:1 results for a sandbox vs your remote server because your sandbox might be more powerful (and it’s usually the case) than your remote server.
    Now as how to install it, all the info its in the docs. If you have further questions about it PM me as I’d like to keep this thread’s topic to Unit Testing ^^

  • #8 / Sep 16, 2009 1:37am

    ronnie_nsu

    23 posts

    Thanks mang!!
    i was using simpletest…but was really hoping for somehting more native to CI!!

  • #9 / Sep 16, 2009 6:41am

    cliffoliveira

    10 posts

    Tks
    is very clear

  • #10 / Oct 13, 2009 5:56pm

    Peyge

    8 posts

    But how can i test my controllers function to knw if everything is okay? I’m just able to test my models..

  • #11 / Oct 13, 2009 8:19pm

    istvanp

    11 posts

    CI does not provide a way to call controllers within another controller so you can’t test them this way (I don’t even know why I added handling it now that you mention it!) But controllers themselves don’t usually need to be tested because they don’t provide one single expected output because it’s typically a view and there is a lot of conditional logic going on. Controllers rely on models, helpers and libraries and as long as those are tested and work, then your controllers should be easy to test manually by visiting them.

  • #12 / Oct 14, 2009 9:05am

    Peyge

    8 posts

    But all the business logic is in the controller so i don’t understant wwhy they didn’t made anything to test them. Maybe un future version of CI! thanks.

  • #13 / Oct 26, 2009 5:35pm

    jazzypants18

    1 posts

    Hey all,

    I’m trying to set up something similar to Jamie Rumbelow’s implementation of SimpleTest, as written here:

    http://jamierumbelow.net/2009/08/11/setting-up-the-perfect-codeigniter-tdd-environment/

    However, I can’t seem to get an example working which allows me to test existing models appropriately.  Let’s say I have an existing model created under application/models/user_model.php and then I create a test under tests/models/user_model_test.php…  When I attempt to run the test for this my model, I’m receiving this error:

    Fatal error: Class ‘Model’ not found in /var/www/vc/system/application/models/user_model.php

    So, obviously, in the test scenario, the code executing in user_model cannot resolve the Model base class.  From this article, I’m gathering that the CodeIgniterUnitTestCase base class attempts to handle this issue, and assuming that simply deriving my test class from CodeIgniterUnitTestCase should be all I need to do to take care of this.  Unfortunately, it doesn’t seem to be working for me.

    Has anyone had any success with this?  The bottom line is that I don’t know how to make the unit tests aware of the resources that are normally autoloaded when running a CI app.  If someone could perhaps be able to provide a stub of a test that would work with a model, it would be greatly appreciated.

    Thanks in advance!

  • #14 / Oct 26, 2009 8:45pm

    Jamie Rumbelow

    546 posts

    You’ve done a great job here Istvan, I’m impressed! It’s a great solution for simple unit testing and it’s great to see you’re encouraging TDD in the community. How far are you going to develop this? Will it turn into it’s own dedicated testing library or are you trying to keep it as close to CI’s as possible?

    The reason why I use SimpleTest is because I get a great range of not only Unit Testing, but also a Web Tester for views. If there could exist a more streamlined solution that ties in with CI’s ethics and code ethos then I’d certainly consider using it as my primary testing solution.

    Jamie

  • #15 / Oct 28, 2009 1:34am

    istvanp

    11 posts

    @Jamie
    So far I am trying to keep it as close as to CI as possible. But I can’t say no to try to extend the capabilities. It will depend on whether or not the project I am working on would need that kind of extensive testing. I actually made a few tweaks already which I am posting below.

    The next thing I might try to do is move from a single controller handling all testing to one that just contains the logic and fetches the test cases in another folder.

.(JavaScript must be enabled to view this email address)

ExpressionEngine News!

#eecms, #events, #releases