A Real-World Example

A Real-World Example

The following example puts to work your knowledge of PHPUnit and Unit Testing frameworks in general.

The component is called xDir. It is a variant of PHP's built-in Dir class (see http://www.php.net/manual/en/class.dir.php). The only real difference is that xDir supports recursion through subdirectories, something that can be immensely useful.

Entirely deliberately, the syntax for xDir is virtually identical to that of the PHP Dir class, with the exception of an optional parameter to specify whether recursion is required.

Those of you with a strong traditional programming background will be familiar with recursion as a programming concept. It simply refers to a function's or method's calling itself in order to perform some operation recursively across a hierarchy. If you've ever played with generating fractals, this will be more than just a little familiar.

Take a look at the following code. It contains a deliberate mistake that you might find difficult to spot.

   <?php    class xdir {      public $path;      public $entries = array();      public $counter = 0;      public $isRecursive;      public function __construct($path, $recursive = false) {        if ((substr($path, strlen($path)-1, 1) == "/") && (strlen($path) != 1)) {         $path = substr($path, 0, strlen($path)-1);        };        $this->path = $path;        $this->isRecursive = $recursive;        if ($this->path) {          $this->_getDirList($this->path);        };      }      public function read() {        if ($this->counter <= (sizeof($this->entries)-1)) {          $s = ($this->entries[$this->counter]);          return($s);          $this->counter++;        } else {          return(false);        };      }      public function isRecursive() {        return($this->isRecursive);      }      public function rewind() {        $this->counter = 0;        return(true);      }      public function close() {        return(true);      }      public function _getDirList ($dirName) {        $objDir = dir($dirName);        if ($objDir) {           while($strEntry = $objDir->read()) {             if ($strEntry != "." && $strEntry != "..") {               if (!(is_dir($dirName."/".$strEntry))) {                 array_push($this->entries, $dirName."/".$strEntry);               } else {                 if ($this->isRecursive) {                   $this->_getDirList($dirName."/".$strEntry, true);                };              };            };          };          $objDir->close();        };      }    };    ?> 

A typical quick-and-dirty test application for this might look like the following:

   <? require_once("xdir.phpm"); ?>    <html>     <head><title>Ed's Quick and Dirty xDir Test App</title>    </head>     <body>      <?php        $objXDir = new XDir("/home/ed/public_html/pacha", true);        while (false !== ($entry = $objXDir->read())) {          echo $entry."<br>\n";         }        $objXDir->close();      ?>     </body>    </html> 

Note that for the purposes of familiarity, this is shamelessly pulled from the example on php.net, right down to the slightly unusual ordering in the while statement. We ask the class to show us the recursive contents of /home/ed/public_html/pacha as the script's output. In fact, this test application would work perfectly if the class actually worked.

Sadly, the class doesn't work, so we get one line of output and then appear to get stuck in an infinite loop. The error log shows nothing because, strictly speaking, as far as PHP's concerned, the script has done nothing wrong.

Where do you go from here? Build a test case. Notice how we've left all our member variables and methods public. This isn't sloppy programming on our part. Rather, it allows us to interrogate our class more easily. After we're satisfied that everything works, we can set what should be private (namely, those three member variables and _getDirList) to be private.

   class MyTestCase extends PHPUnit_TestCase    {        var $objXDirClass;        function __construct($name) {           $this->PHPUnit_TestCase($name);        }        function setUp() {            $this->objXDirClass = new XDir("", "true");        }        function tearDown() {            unset($this->objXDirClass);        }        function testRead() {            $this->objXDirClass->counter = 1;            $intCounterBefore = $objXDirClass->counter;            $this->objXDirClass->entries = array("/home/ed/test1", "/home/ed/test2",    "/home/ed/test3", "/home/ed/test4");            $strActualResult = $this->objXDirClass->read();            $intActualCounterAfter = $this->objXDirClass->counter;            $strExpectedResult = "/home/ed/test2";            $intExpectedCounterAfter = 2;            $this->assertTrue(($strActualResult == $strExpectedResult) &&    ($intActualCounterAfter == $intExpectedCounterAfter));        }      } 

Notice how we're testing only one method here the read method. In practice you should ideally devise a test for every method in the class, including _getDirList, but because _getDirList talks directly to the file system, it would be virtually impossible in practice to falsify its input in order to test its expected output against actual output. Go ahead and test the first method, anyway, and see whether it's the culprit. If it's not, you can always return to _getDirList later.

A quick word on the logic in play here. First, we are setting our array counter to be 1. This counter variable refers to the class's current position in its list of matching files (starting at 0 - so 1 is the second position). We're then defining a falsified list of matching files test1 thru test4, for convenience.

What we expect to get back from the method is the second file in the list, which is test2. We also expect the counter to tick forward by one, from 1 to 2. We apply these two tests as an assertion. If the assertion fails, we know that one of our two tests is wrong.

Build your test suite. Assume that you saved your test case as testcase.phpm. Create the following script and call it testsuite.php:

   <?php    require_once 'xdir.phpm    require_once 'testcase.phpm;    require_once 'PHPUnit.php';    $objSuite = new PHPUnit_TestSuite("MyTestCase");    $strResult = PHPUnit::run($objSuite);    print $strResult->toString();    ?> 

Run this at the command line and you'll see something like the following:

   TestCase mytestcase->read() failed: expected true, actual false 

This is clearly both a good and a bad result. You now know that it is in fact the read() method that is causing the infinite loop, but you don't yet know why (though you could guess). Just to be sure, modify the last line of the testRead() method so that it looks like this:

   $this->assertTrue($strActualResult == $strExpectedResult); 

Run it again and you get:

   TestCase mytestcase->testRead() passed 

You can deduce from this that the test that is actually failing is the test between the expected value of the counter variable and the actual value of the counter variable after the read() method has been called. Modify the last line of the testRead() method once more, to read:

   $this->assertEquals($intActualCounterAfter, $intExpectedCounterAfter); 

Run it again and you get:

   TestCase mytestcase->testRead() failed: expected 2, actual 1 

You can see that the counter variable is not being incremented as you expect it to be. You expect it to be 2 but are in fact getting 1. What's going on? Well, at least now you know where to look. Turn back to the read() method of your original code for xDir:

   public function read() {        if ($this->counter <= (sizeof($this->entries)-1)) {                $s = ($this->entries[$this->counter]);                return($s);                $this->counter++;        } else {                return(false);        };    } 

Can you see what it is yet?

That's right. The return() for the function is being called before the counter is incremented. PHP won't execute any code after the return() statement. As a result, the counter increment is never actually executed at all.

Revise the code to swap the position of the return() and counter increment statements:

   public function read() {            if ($this->counter <= (sizeof($this->entries)-1)) {                     $s = ($this->entries[$this->counter]);                     $this->counter++;                     return($s);            else {                     return(false);            };    } 

And run the test once more:

   TestCase mytestcase->testRead() passed 

Go back to the original test the one that combined the two vital tests with assertTrue. Check that this, too, now gives a good result:

   TestCase mytestcase->testRead() passed 

Everything now looks good.

In theory, this is all you need to do to be completely sure this method works. But if you need the added psychological reassurance, you can always refer back to the quick-and-dirty script, which should now execute fine without problems. Run it in your Web browser and you'll get something similar to:

   /home/ed/public_html/pacha/perl/cleanbuf.sh    /home/ed/public_html/pacha/perl/cleandb.pl    /home/ed/public_html/pacha/perl/deploylocaldata.sh    /home/ed/public_html/pacha/perl/mailfwd.pl    /home/ed/public_html/pacha/perl/oldphotouploads.pl    /home/ed/public_html/pacha/perl/pafserver.pl 

As you can see, you've made it, and your PHPUnit testing framework was vital in helping you track down that hard-to-spot bug. Can you imagine how long it would have taken to fudge through that using var_dump and error_log?

Professional PHP5 (Programmer to Programmer Series)
Professional PHP5 (Programmer to Programmer Series)
Year: 2003
Pages: 182

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net