[Previous entry: "File permissions and HTTPD"] [Next entry: "UGOT: A familiar approach to testing from Frank Cohen"]
09/28/2004: "PHP: pass by value/pass by reference"
After years of programming in languages where passing object references around is the norm, I just spent about 30 minutes re-learning the distinction between pass by value and pass by reference. The good news is that I did so within the context of writing tests, so it wasn't so bad.
It boils down to this: PHP passes function parameters by value, so unless you say otherwise, the function will operate on a copy of its parameters, and changes to an object's state are not visible by the invoker. If you want to pass a parameter by reference, you must say so. I came across this while rolling my own test runner in PHP.
I started with a class TestRunner that keeps track of test results. To execute a test, the test runner invokes a test method and passes itself as a parameter to the test. The test then signals failure back to the test runner when a test fails. Here's a little code. First, the Test Runner:
class TestRunner {
var $passed;
var $failed;
var $tests;
function TestRunner() {
$this->passed = 0;
$this->failed = 0;
$this->tests = array();
} function addTest($test) {
$this->tests[] = $test;
}
function signalFailed() {
$this->failed++;
}
function runTests() {
foreach ($this->tests as $i => $eachName) {
call_user_func($eachName, $this);
}
}
function report() {
echo count($this->tests) . " run, " . $this->passed . " passed, " . $this->failed . " failed.";
}
A test is just a global function, such as this one:
function testNullStringIsEmpty($testRunner) {
$testRunner->signalFailed();
}
Here, $testRunner->signalFailed() is the equivalent to xUnit's fail() method.
The problem is that when I run the tests, the report claims 0 tests failed, even though clearly one of the tests failed. Sadly, I had to debug:
- It happily reports "1 run, 0 passed, 0 failed," so I know the test runner has the test to execute.
- When I add
echo "I got here."inside the test function, I confirm that the test is executing. - When I add
echo "I got here."insidesignalFailed(), I confirm that the test tries to signal its failure. - When I add
echo $this->failedinsidesignalFailed(), I get 1. - When I add
echo $this->failed after executing the test inside runTests(), I get 0! RED BAR
I read the PHP manual and it has a section on "References Explained". Hm. That sounds like that will help. I read this:
You can pass variable to function by reference, so that function could modify its arguments.
The syntax is clear: use the ampersand (&) before the function parameter name to declare it "pass by reference". I did that.
function testNullStringIsEmpty(&$testRunner) {
$testRunner->signalFailed();
}
I ran the tests. No change. Strange! But the manual clearly says:
Note that there's no reference sign on function call - only on function definition. Function definition alone is enough to correctly pass the argument by reference.
Well, let me try changing the function call site anyway. It can't hurt. I did that.
class TestRunner {
function runTests() {
foreach ($this->tests as $i => $eachName) {
call_user_func($eachName, &$this);
}
}
Hey, it worked! Just for fun, I changed the function parameter back.
function testNullStringIsEmpty($testRunner) {
$testRunner->signalFailed();
}
Hey, it still worked! It seems that I have to do the exact opposite of what the manual claimed. Shame.
Fortuantely, the PHP manual is a kind of Wiki, so I'll post my addition to the manual, and perhaps it (along with this entry) will help someone. We'll see!
