Part of the EllisLab Network
   
1 of 3
1
Improved CodeIgniter Unit Testing
Posted: 15 September 2009 10:46 PM   [ Ignore ]  
Summer Student
Avatar
Total Posts:  12
Joined  05-04-2009

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)—

File Attachments
unit_test.zip  (File Size: 4KB - Downloads: 257)
Profile
 
 
Posted: 15 September 2009 11:01 PM   [ Ignore ]   [ # 1 ]  
Summer Student
Avatar
Total Posts:  12
Joined  05-04-2009

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—

Profile
 
 
Posted: 15 September 2009 11:01 PM   [ Ignore ]   [ # 2 ]  
Summer Student
Avatar
Total Posts:  12
Joined  05-04-2009

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''admin@localhost.com''Administrator');
    
$this->unit->run($testTRUE$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($testTRUE$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!

Profile
 
 
Posted: 15 September 2009 11:48 PM   [ Ignore ]   [ # 3 ]  
Grad Student
Rank
Total Posts:  49
Joined  02-16-2009

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

 Signature 

need Bali makeup Artist and Wedding?
please visit : http://riasbali.com

Profile
 
 
Posted: 16 September 2009 12:00 AM   [ Ignore ]   [ # 4 ]  
Summer Student
Avatar
Total Posts:  12
Joined  05-04-2009

@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.

Profile
 
 
Posted: 16 September 2009 12:22 AM   [ Ignore ]   [ # 5 ]  
Grad Student
Rank
Total Posts:  49
Joined  02-16-2009

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

 Signature 

need Bali makeup Artist and Wedding?
please visit : http://riasbali.com

Profile
 
 
Posted: 16 September 2009 12:34 AM   [ Ignore ]   [ # 6 ]  
Summer Student
Avatar
Total Posts:  12
Joined  05-04-2009

@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 ^^

Profile
 
 
Posted: 16 September 2009 12:37 AM   [ Ignore ]   [ # 7 ]  
Grad Student
Rank
Total Posts:  34
Joined  08-31-2008

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

Profile
 
 
Posted: 16 September 2009 05:41 AM   [ Ignore ]   [ # 8 ]  
Summer Student
Avatar
Total Posts:  13
Joined  08-15-2009

Tks
is very clear

 Signature 

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

Profile
 
 
Posted: 13 October 2009 04:56 PM   [ Ignore ]   [ # 9 ]  
Summer Student
Avatar
Total Posts:  10
Joined  10-09-2009

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

 Signature 


How to kill a programmer in the shower? Make him read a shampoo bottle:
  -Wash hair
  -Rince
  -Repeat
...

Profile
 
 
Posted: 13 October 2009 07:19 PM   [ Ignore ]   [ # 10 ]  
Summer Student
Avatar
Total Posts:  12
Joined  05-04-2009

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.

Profile
 
 
   
1 of 3
1