Testing Php

Discussion of testing theory and practice, including methodologies (such as TDD, BDD, DDD, Agile, XP) and software - anything to do with testing goes here. (Formerly "The Testing Side of Development")

Moderator: General Moderators

DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Testing Php

Post by McGruff »

One of the benefits of testing is that tests provide a source of documentation. A unit test gives a class a thorough workout describing its behaviour in a variety of circumstances in great detail. Compared to ordinary docs, tests never get out of sync with the code. If something is edited and one of the test assertions is no longer valid, you'll instantly be alerted with a failing test.

As well as testing your own code, you can also test php itself. It's a good way to make some notes about a relatively obscure piece of behaviour which isn't immediately obvious and you can add to this at any time if further issues come to mind.

A trivial example would be (using SimpleTest):

Code: Select all

 class TestOfInArray extends UnitTestCase{    function TestOfInArray()    {        $this->UnitTestCase();    }    function test()    {        $array = array('foo');        $this->assertIdentical(in_array('foo',$array), true);        $this->assertIdentical(in_array('bar',$array), false);    }} 
Not terribly useful - I'll show you a better example in a moment. First, there's an important point to make about testing. Tests are normally written before the code which is to be tested. You start with a failing test which expresses some requirement or other for an implementation, then you write just enough code to pass the test. In this way the code follows behind the test (test-infected programmers don't really write code: they write tests and the code emerges from the tests).

It's a big change in perspective but a really nice way to work. By deciding requirements first and then coding to these you stay focussed. If, for example, you just want to display a "happy birthday" message for site visitors, that's all you do. You don't go off on a mission to create a comprehensive calendar system which can be used for personal organisers, project planning, or whatever else you might want to do involving times and dates. These could be useful classes to have in your library but that can wait.

If you use unit tests to document the behaviour of native php functions, you're turning the test-first-then-code idea on its head. Rather than the code following the tests, the tests follow the code. In the above test you can't edit php to make the test work, you have to alter the assertions until they pass. That's emphatically *not* the way you would normally use a testing framework.

With that out of the way, here's a more useful example of using unit tests to document php. It stems from a "DeleteBranch" class I was working on. It's intended to be a convenient way to delete an entire filesystem branch with a single command, mimicing the behaviour of the unix rm -rf command. There were a number of issues which I had to investigate: does php treat unix symbolic links and windows shortcuts in the same way? Does the dir() pseudo object follow symlinks/shorcuts?

Time to test. First, you need to make the following filesystem branch in Windows and unix. In the test case, set values for $this->_test_root for each OS. The branch contains some files, dirs and a couple of links: one to a file (b_shortcut.txt) and one to a dir (bar_shorcut). Obviously these should be symlinks on unix and shorcuts on windows.

Code: Select all

     test root    |    |    foo   a.txt   b_shortcut.txt[.lnk]   bar_shorcut[.lnk]    |    |    bar    |    |    b.txt 

Code: Select all

 class TestOfSymLinks extends UnitTestCase{    function TestOfSymLinks()    {        $this->UnitTestCase();    }    function setUp()     {        if($this->_isWin()) {            $this->_test_root = 'e:\\for_symlinks_tests\\';   # your own settings here        } elseif($this->_isNix()) {            $this->_test_root = '/home/user/php/for_symlinks_tests/';        } else {            trigger_error('OS not recognised - please add to list.');        }    }    function testIsLink()     {        if($this->_isWin()) {            $this->assertIdentical(                is_link($this->_test_root . 'bar_shortcut.lnk'),                 false); #!!            $this->assertIdentical(                is_link($this->_test_root . 'b_shortcut.txt.lnk'),                 false); #!!        } elseif($this->_isNix()) {            $this->assertIdentical(                is_link($this->_test_root . 'bar_shortcut'),                 true);            $this->assertIdentical(                is_link($this->_test_root . 'b_shortcut.txt'),                 true);        }    }    function testIsFile()     {        if($this->_isWin()) {            $this->assertIdentical(                is_file($this->_test_root . 'a.txt'),                 true);            $this->assertIdentical(                is_file($this->_test_root . 'b_shortcut.txt.lnk'),                 true);                } elseif($this->_isNix()) {            $this->assertIdentical(                is_file($this->_test_root . 'a.txt'),                 true);            $this->assertIdentical(                is_file($this->_test_root . 'b_shortcut.txt'),                 true);        }    }    function testIsDir()     {        if($this->_isWin()) {            $this->assertIdentical(                is_dir($this->_test_root . 'foo'),                 true);            $this->assertIdentical(                is_dir($this->_test_root . 'bar_shortcut.lnk'),                 false); #!!        } elseif($this->_isNix()) {            $this->assertIdentical(                is_dir($this->_test_root . 'foo'),                 true);            $this->assertIdentical(                is_dir($this->_test_root . 'bar_shortcut'),                 true);        }    }    function testFileType()    {        if($this->_isWin()) {            $this->assertEqual(                filetype($this->_test_root . 'foo'),                 'dir');            $this->assertIdentical(                filetype($this->_test_root . 'bar_shortcut.lnk'),                 'file'); #!!            $this->assertEqual(                filetype($this->_test_root . 'a.txt'),                 'file');            $this->assertIdentical(                filetype($this->_test_root . 'b_shortcut.txt.lnk'),                 'file'); #!!        } elseif($this->_isNix()) {            $this->assertEqual(                filetype($this->_test_root . 'foo'),                 'dir');            $this->assertEqual(                filetype($this->_test_root . 'bar_shortcut'),                 'link');            $this->assertEqual(                filetype($this->_test_root . 'a.txt'),                 'file');            $this->assertEqual(                filetype($this->_test_root . 'b_shortcut.txt'),                 'link');        }    }    function testCanonicalExpansion()     {        if($this->_isWin()) {            $this->assertNotEqual(                realpath($this->_test_root . 'b_shortcut.txt.lnk'),                 $this->_test_root . 'foo/bar/b.txt');             #!!         } elseif($this->_isNix()) {            $this->assertEqual(                realpath($this->_test_root . 'b_shortcut.txt'),                 $this->_test_root . 'foo/bar/b.txt');        }    }    function testDirPseudoObjectFollowsSymlinkToTarget()     {        if($this->_isNix()) {            $it =& dir($this->_test_root . 'bar_shortcut');            $contents = array();            while(false !== ($item = $it->read())) {                $contents[] = $item;            }            $it->close();            $this->assertIdenticalArrayValues($contents, array('.', '..', 'b.txt'));        }    }    // it's not a valid path without .lnk    function testWinShortcutsMinusTheLnkExtension()     {        if($this->_isWin()) {            $this->assertIdentical(is_link($this->_test_root . 'bar_shortcut'), false);            $this->assertIdentical(is_link($this->_test_root . 'b_shortcut.txt'), false);            $this->assertIdentical(is_file($this->_test_root . 'b_shortcut.txt'), false);            $this->assertIdentical(is_dir($this->_test_root . 'bar_shortcut'), false);            $this->assertIdentical(filetype($this->_test_root . 'bar_shortcut'), false);            $this->assertIdentical(filetype($this->_test_root . 'b_shortcut.txt'), false);            $this->assertNotEqual(                realpath($this->_test_root . 'b_shortcut.txt'),                 $this->_test_root . 'foo/bar/b.txt');        }    }    function _isNix()     {        return preg_match('/linux/i', $_SERVER['SERVER_SOFTWARE']);    }    function _isWin()     {        return preg_match('/win/i', $_SERVER['SERVER_SOFTWARE']);    }}  
The first thing to note is that Windows shortcuts have a .lnk extension but this is hidden in windows explorer even if you have show extensions & show hidden files toggled on. Paths without the .lnk suffix are simply invalid.

In Windows you can see that:
(1) realpath cannot expand windows shortcuts
(2) shortcuts are never identified as links on windows.
- shortcuts to files have type 'file' not 'link'
- shortcuts to dirs are also type 'file' ie they're not following the shortcut to its target

In unix, the dir() pseudo object will indeed follow symlinks to their target. This was important to know for my DeleteBranch class. This reads through a branch recursively, and so could easily end up deleting much more than you bargained for if it follows links...

On unix, an is_dir($path) check will return true for real dirs and symlinked dirs but if('dir' == filetype($path)) will only return true for dirs. The latter allows you to filter out symlink paths, should you need to.

The test case provides a source of documentation which I can refer to later when I forget the results, as I inevitably will. With a written test in place, I can add to it later if further twists come to mind, building up my knowledge. I can also run this against new versions of php to check if windows behaviour might have been brought into line with unix.

The phpv4.4 "update" caused a lot of problems for OOP programmers. You have to declare a new object as a variable before returning it rather than returning the new object directly. Tests might provide some early warning for this kind of thing. It would be a mammoth task to try to cover all of php with tests of course. If only the developers had written a manual expressed as test cases...
DevNet Master
Posts: 4872
Joined: Wed Feb 11, 2004 3:23 pm
Location: Palm beach, Florida

Re: Testing Php

Post by josh »

Your examples don't show up
User avatar
Site Administrator
Posts: 6911
Joined: Sun May 19, 2002 10:24 pm

Re: Testing Php

Post by Benjamin »

:arrow: Old Thread = Locked