Test Driven Development
We have begun coding via test driven development in the current project I am on. It is a dicipline that results in well written, "clean" code that works and has the added benefit that when changes are made in the future, the tests can be re-run to check that nothing has been broken.
The process of TDD encourages code development that is portable and uncoupled from dependencies.
The project is based on the Codeigniter framework which I discovered has it's own unit testing built in. The difference between PHPUnit and Codeigniter unit testing is faily large, but the critical thing is that you get on with TDD regardless.
Codeigniter Unit Testing vs PHPUnit Testing
Depth of the tests
Codeigniter: limited
PHPUnit: extensive
Dependency Friendly
Codeigniter: allows the codeigniter framework dependencies
PHPUnit: must be uncoupled from all dependencies
Overall, the Codeigniter unit testing is forgiving of the Codeigniter framework, allowing a quick start to test driven development when already deep into the CI code pattern. PHPUnit provided a lot more testing capabilities such as the ability to test exceptions that have been thrown etc.
I started with PHPUnit and quickly discovered the dependency requirements through running into errors based on the tight Codeigniter dependency coupling...reliance on the $this->
object etc.
I then backed up and proceeded with a complete round of Codeigniter unit testing using the code class below which was very forgiving, and which also allowed me to test the code quite thoroughly first.
After that, I worked out the bugs using PHPUnit, knowing that the code was already tested, and proceeded to uncouple all the needed libraries etc, and repeated all the tests again using PHPUnit.
The change allowed me to then introduce a few new, complex tests that CI could not provide.
The process of test driven development:
- You first write a test for a function before you write the function itself
- You create a failing test initially, rounding this out with passing tests.
- You then build the actual functionality of the function.
- Run the tests to see that you get the expected results.
- After that you wrinse and repeat.
To describe it in another way:
coupling clean code with test driven development takes a larger, complex problem, divides it into many single use functions (solutions to small problems), where each is individually tested, and at the end of the process, a new parent function will call all the well written, tested, simple functions to complete the larger, overall task.
Each simple, small function will be known to handle it's task, bad data etc well, so your ability to deliver code that does not break is tremendously improved.
You would also unit test this overall, final function following the same process, but having a more complex "known" environment.
I have included a brief how to for Codeigniter unit testing here including the class I wrote that helps make the running of all future tests quite simple.
This is written to use the HMVC modules extension, so the path's etc are related to that.
To get started, follow these steps:
1. create a new module folder application folder/modules/unit_tests
2. create your controller folder within that.
3. create a php file called run.php within the controller folder, and place the class code directly below this in that file.
4. add new test class files as needed...example, payroll_tests.php for testing payroll, sales_tests.php etc. The name does not matter. Each public function within these files with test__
in it's name will be run automatically.
5. to run all tests, follow this URL: http://yourProjectDomain/unit_tests/run/all
Note: I have authentication built into my class extends Base_Authorized_Controller
, do remove that and add your specific code.
Base Codeigniter Unit Test Class (run.php) Content
<?php if ( ! defined('BASEPATH')) exit ('No direct script access allowed');
//========================================
// File :
// Author : Jonathan Galpin jonathan@iqzero.net
// Date : Feb 2013
//========================================
// unit test run class
//
// place test classes in modules/unit_tests/controllers
// all functions named "test__" will be run when the all function on this page is run
// http://yourProjectUrl.com/unit_tests/run/all
//========================================
require_once( APPPATH . "modules/pages/controllers/base/base_authorized_controller.php" );
class run extends Base_Authorized_Controller
{
private $test_class_file_array;
private $exclude_files = array( ".", "..", "run.php" );
public function __construct()
{
parent::__construct();
$this->load->library('unit_test');
$this->unit->use_strict(TRUE);
$path = APPPATH . "modules/unit_tests/controllers";
$this->test_class_file_array = scandir( $path );
foreach( $this->test_class_file_array as $file )
{
if( !in_array( $file, $this->exclude_files) ) require_once $file;
}
}
public function all()
{
foreach( $this->test_class_file_array as $file )
{
if( !in_array( $file, $this->exclude_files) )
{
$class = explode(".", $file);
$this->run_tests_on_class( new $class[0]() );
}
}
$this->display_results( $this->unit->result() );
}
//========================================
// private functions
//========================================
private function run_tests_on_class( $class )
{
$class_methods = get_class_methods( $class );
// loop over each methods
foreach($class_methods as $method)
{
// if the class functions start out with test__, then run those functions
$pattern = '/^test__/';
if(preg_match($pattern, $method))
{
$class->$method();
}
}
}
private function display_results( $results )
{
$html_out = date("Y-m-d H:i:s A");
$html_out .= '
<table border="0" cellpadding="4" cellspacing="1">
<tr>
<td>Test #</td>
<td>Test Name</td>
<td>Result</td>
<td>File</td>
<td>Line</td>
</tr>';
$item_number = 0;
foreach( $results as $result )
{
$item_number++;
$pass_fail_css_color = ( $result["Result"] == "Passed" ? "black" : "red" );
$class_file = explode("/", $result["File Name"] );
$html_out .= "
<tr style=\"color:{$pass_fail_css_color}\">
<td>{$item_number}</td>
<td>{$result["Test Name"]}</td>
<td>{$result["Result"]}</td>
<td>{$class_file[9]}</td>
<td>{$result["Line Number"]}</td>
</tr>";
}
$html_out .= '</table>';
echo $html_out;
}
} // end class
?>
Example Test File
<?php if ( ! defined('BASEPATH')) exit ('No direct script access allowed');
class test_class extends MX_Controller
{
public function test__pass_test()
{
$this->unit->run( 1, 1, 'pass test' );
}
public function test__fail_test()
{
$this->unit->run( 1, 2, 'fail test' );
}
}
?>
More Extensive Example
This is a closer to real world example of the Codeigniter unit testing. We are handling payroll, and one of the problems to solve is to obtain the next pay date based on the past pay date and whether or not there is one or two weeks in the pay-period.
- Create a test file called payroll_test.php with the same class name within.
- Create a public function called
test__calculate_next_pay_date_two_weeks()
- Set up a known environment...ie the date you feed in, the resulting date you expect to receive back, and if you expect it to pass or fail. To do this, I call a private function that returns an array
$test_last_date_and_expected_result_array = $this->get_every_other_week_test_array();
. - Loop thought the array calling the library function that will produce the results, executing the test for each scenario passed in (at this point, you may have the actual function shell, but no code. As a result, all tests should fail initially).
By including a test result, you can then also throw in tests that are expected to fail. This would also allow you to test the handling of non dates, nulls etc although the prefered response should be to throw an exception when this is critical to the code and CI's unit testing is not suited to that. In that event, you may have to catch the exception, then check your results to see if that was the result you needed.
Here is the closer to real world test code:
public function test__calculate_next_pay_date_two_weeks()
{
$test_last_date_and_expected_result_array = $this->get_every_other_week_test_array();
foreach( $test_last_date_and_expected_result_array as $test )
{
$test = (object)$test;
$next_pay_date = $this->payroll_library->calculate_next_pay_date( $test->last_date, 2 );
$this->unit->run( $next_pay_date, $test->expected_date, __FUNCTION__ . " {$test->last_date} => {$test->expected_date}");
}
}
private function get_every_other_week_test_array()
{
return array(
array(
"last_date" => "2012-07-12",
"expected_date" => "2012-07-27",
"expected_result" => FAIL
),
array(
"last_date" => "2012-07-26",
"expected_date" => "2012-08-09",
"expected_result" => TRUE
),
array(
"last_date" => "2012-08-09",
"expected_date" => "2012-08-23",
"expected_result" => TRUE
),
array(
"last_date" => "2012-12-27",
"expected_date" => "2013-01-10",
"expected_result" => TRUE
),
array(
"last_date" => "2013-01-10",
"expected_date" => "2013-01-24",
"expected_result" => TRUE
),
array(
"last_date" => "2013-01-24",
"expected_date" => "2013-02-07",
"expected_result" => TRUE
),
array(
"last_date" => "2013-12-26",
"expected_date" => "2014-01-09",
"expected_result" => TRUE
),
array(
"last_date" => "2014-01-09",
"expected_date" => "2014-01-23",
"expected_result" => TRUE
),
);
}
The key line for the CI unit test in the loop above is this:
$this->unit->run( $next_pay_date, $test->expected_date);
To use the expected result part of the array (allowing you to test for known failures), you would call it in this fashion:
$this->unit->run( ($next_pay_date === $test->expected_date), $test->expected_result );
Be careful to use a type safe comparison such as ===
.
- Create the library that will handle the business logic, in this case payroll_library.php
- Create the public function called
calculate_next_pay_date( $last_pay_date, $weeks_in_pay_period )
Bottom line, it definitely takes more time to do test driven development, but it results in better, working code that will alert you later to problems when the project is modified years later.
If you have to use the less capable Codeigniter unit testing, this is ok in that it is far better than doing no unit testing. Your code will be infinitely better off for unit testing regardless of the framework used.