Code As Data: Reflection in PHP
Pages: 1, 2
Code Generation
Once the test cases have been created via the user interface, all that is left is to actually build the code. This is a fairly simple operation in terms of setting up the code for each test and then running it. If the method returns the expected result, then the test passes; if not, then it fails. The tests are run in order by a test framework. For each test, a simple function must be created to create the object, call the method, check the result, and then do any cleanup that is needed. Each test will be a simple function with no parameters. The test functions will return PASS or FAIL.
The tests are built by a command-line tool. The tool reads in the XML file for the tests to be generated, and then outputs a PHP file containing the tests, which may then be run with the test runner script.
Each test is implemented as a PHP function with no parameters. The test runner will take a list of those functions and run them in order. If a test passes it will return the constant "PASS"; if it fails, it will return "FAIL." The test runner script can then iterate over all of the tests and return the data to the user. By default, it will run at a command line with the test name followed by "PASS" or "FAIL." It may also be useful to log how long each test took to run.
The basic format of each test is to call the class constructor (with any supplied parameters), then check that the constructor returned a valid object of the correct type. If it did not, the test will fail (unless that is the expected outcome). Then it calls the method to be tested. The test will then check that all of the assertions are true.
There are three types of possible assertions. The first is a return value, which can be true, false, null, a string, numeric value, a specific result, or anything else that can be returned by a PHP function. Any of these can be tested for. The test will PASS if the return value is what was expected, and FAIL otherwise.
The second type is to check whether a class value is set. A class property assertion can test for the same types of values as a return value assertion. In some cases, there will be more than one class property assertion, or a return value assertion and a property assertion. In this case, they both must be satisfied.
The third type of assertion is an exception. This can be a general Exception or a specific type of Exception. In this case, a test will pass if the expected Exception happens, and fail otherwise.
If there are several assertions for a test, then all of them must be satisfied for the test to PASS.
The test runner will have an ordered list of all the test functions. It will then run them in order. If a test fails, the runner will stop and report the error.
Here is the code to generate the tests:
public function buildClassTests($class)
{
$methods = $class->xpath('method');
$tests = array();
foreach($methods as $method)
{
$tests[] = build_Tests($method);
}
$filename = $class['name'] . ".tests.php";
$test_file = join('',$tests);
file_put_contents($filename, $test_file);
}
private function build_tests($method)
{
$tests = $method->xpath('test');
$test_name = $method['name'];
foreach($test as $test)
{
$test_name = $test['name'];
$parameters = $test->xpath('cons_parameter');
foreach($parameter as $param)
{
if($param['value'] == 'DEFAULT')
break;
$cons_p[] = "'".$param['value'] .".";
}
$parameters = $test->xpath('parameter');
$p = array();
foreach($parameter as $param)
{
if($param['value'] == 'DEFAULT')
break;
$p[] = "'" . $param['value'] ."'";
}
$p = join(',',$p);
$assert = array();
$exception = 'Exception';
$exceptionReturn = 'FAIL';
$assertions = $test->xpath('assertion');
/* Current code only supports one assertion, but it may sometimes be useful to have more than one*/
foreach($assertions as $a)
{
switch($a['type'])
{
case 'return':
$assert[] = sprintf("%11s%20s%4s%20s","",'$result',$a['cond'],$a['value']);
break;
case 'exception':
$exception = $a['exception'];
$exceptionReturn = 'PASS';
}
}
if($exception != 'Exception')
$exep = " catch (Exception $e) \n".
" {\n".
" return FAIL;".
" \n}\n";
else
$exep = "";
if($assert)
{
$assert_block = " if(\n". join("&&\n",$assert) . " )\n";
$assert_block = " return PASS\n";
$assert_block = " else\n";
$assert_block = " return FAIL\n";
}
$code = <<<CODE
function test_$test_name()
{
try
{
/* this is outputting php code, so the $ signs need to be escaped */
\$class = new $ClassName();
\$result = \$class->$name($p);
$assert
}
catch ($exception \$e)
{
return $exceptionReturn
}
$excp
CODE;
$tests[] = $code;
}
return join("\n\n\n",$tests);
}
This is a simple test case. If the method returns true, it passes; if the method returns false or throws an exception, it fails.
function test_test4()
{
try
{
$class = new testClass();
$result = $class->execute('12345');
if($result == true)
return PASS;
else
return FAIL;
}
catch (Exception $e)
{
return FAIL;
}
Here is the generated code for the listing code runner:
<?php
$tests = array(list of tests);
$i = 0;
$fail = false;
foreach ($tests as $test)
{
$i++;
$result = $test();
if($result == PASS)
echo "$i Test $test PASS<br/>";
else
{
echo "<span class='fail'>$i Test $test FAIL</span><br/>";
$fail = true;
break;
}
}
if($fail)
echo "SOME TESTS FAILED";
else
echo "ALL TESTS PASSED";
?>
Zachary Kessin has worked with free software and web development for more than ten years. He is a frequent speaker at Jerusalem.pm and has spoken at YAPC::Israel.
Return to PHP DevCenter.
You must be logged in to the O'Reilly Network to post a talkback.
