Before I write too much PHP code, I want to get my TDD environment sorted out. Prior to my current job, I'd never done unit testing before (cringe), but I'm well onboard with it as a concept. Initially it seemed arse-about-face to write tests for code that doesn't exist yet, but it really is a good approach to writing solid code, and not going to OTT with features one doesn't need (or doesn't need yet), as it makes for more testing before the unnecessary code gets written. And much as I'm OK with writing tests, I only want to write what's necessary.
I have to admit that for my after-hours work I only write unit tests for about 50% of it, and I generally write them afterwards. Mostly due to slackness, and mostly due to not really caring whether my code for this blog and other things I potter around with works robustly. But any work I do for paying clients gets written in a correct TDD fashion. And they get charged for it, and they're happy with doing so. I hasten to add I do very very little paid-for work in a "moonlighting" capacity, indeed have not accepted any for a coupla years now.
Anyway, getting a unit testing environment up and running for CF is bloody easy. One does this:
- go to the MXUnit website and download it;
- stick it in a web-accessible directory;
- add a mapping for mxunit if it's not already in the web root;
- in Eclipse install the plug-in (instructions provided on the page above);
- tell Eclipse where the MXUnit test runner is located.
I presumed it would be a similarly easy process with PHP. Hmmm. No. Well maybe it is if one knows what one is doing, but I don't fall into that demographic, I'm very much in the position of using Google to find instructions, and then follow the instructions often not knowing why I am doing whatever it is the instructions are telling me to do. Fortunately I am never the first person to take this approach, and any problem I have has been solved by someone else.
I have to say that a lot of the docs I have found for doing PHP stuff have been written from the perspective of writing notes for someone who already knows what the steps actually are, rather that from the perspective of someone not having a clue. This has been especially the case with the process I am about to document here.
Unlike my previous article on starting from scratch and ending with "Hello World" written in PHP, I have already undertaken the process I am gonna document here, indeed I have done it two times trying to work out why some things happen the way they do. However I'm not happy with the end results so I am going to start again, and document my latest attempt.
One good thing about "blowing it away and starting from scratch" with PHP is that one can just delete the PHP directory and replace it with a fresh copy from my unzipped download. There's no "installation" per se, beyond the file copy. For expedience I take a copy my php.ini first as it's got a coupla custom settings in it. Contrast this with being faced with doing a similar thing in CF: needing to deinstall and reinstall. What a tedious, long-winded process.
Picking a unit testing framework
OK, starting from scratch, I hadn't even decided that PHPUnit was the way to go here. I was aware it existed, but suspected that along with most solutions to PHP questions, there was more than one way to skin a cat. So I googled "PHP unit test framework" to see what was suggested.From the Google results there seemed to be three contenders:
- PHP Unit Testing Framework (imaginitive name, that one);
- SimpleTest
- PHPUnit
And a coupla questions on StackOverflow, one asking "which unit-test framework for PHP: simpletest, phpunit or?", and one "Best way to implement unit testing in PHP". Both of those seemed to err towards PHPUnit, whilst speaking favourably about SimpleTest too. But I decided to go with my own initial instinct - backed up by the general consensus StackOverflow seemed to have, and decided to go for PHPUnit.
Installing PHPUnit
The docs on the project home page for PHPUnit offer the installation options, none of which are "just check out the code and make it runnable" like with MXUnit, which is disappointing. And the three installation options given meant nothing to me, being a complete n00b when it comes to PHP. So time to RTFM some more. Sigh. I have the following options:- Use "PEAR"
- Use "Composer"
- Use a PHP archive
PEAR
As per the link above, PEAR is a command line installer for installing PHP apps. I had a flick through the documentation, and landed on the install instructions.I fell at the first hurdle, because the docs say this:
After you have downloaded and installed PHP, you have to manually execute the batch file located in e.g. c:\php\go-pear.bat.
OK, that's nice. Well it would be if I had a go-pear.bat file in my PHP dir. I did some googling around and found out it's missing from the 5.4 zip, so I need to install it differently. I cannot remember what I was googling to find this out, nor the page that told me this. Anyway, further down the instructions page it advises how to update PEAR, and I figured perhaps this would also allow me to install it (later confirmed as being correct by my googling).
- I browsed to http://pear.php.net/go-pear.phar and downloaded the file, saving it in my D/L dir;
- I open a command prompt as administrator, and drilled down to my PHP dir, and executed the command to "run" the phar file:
This is after accepting the first prompt, "Are you installing a system-wide PEAR...", to which I accepted the default.
Now here's my problem. I have to decide what to do with all these directory options. I could accept the defaults, and the first time I did, but then it goes and installs PEAR directly in my PHP dir, which doesn't seem right to me. It does not seem right to me to install one app inside the directory of another app. It doesn't even install in a sub-directory "pear", it just slaps all its files amongst the rest of the PHP files. Messy.
My file structure is such that I have a C:\apps\php directory, within which are two different flavours of PHP itself: the thread-safe one that Apache needs to use, and the non-thread-safe one that IIS needs to use. So I figure that given PEAR is PHP-related, installing it to C:\apps\php\PEAR seems like a reasonable idea. So I'll set all the directories accordingly.
using that installer UI, I've been having a lot of problems getting the directories set the way I want... if I set them in the wrong order, it seems to break the installer. I wanted to change the first path (to C:\apps\php\PEAR), but as soon as I did, the script stopped working in that I could no longer change any of the other settings, instead getting an error with some VBS script not being locatable. The only other thing I wanted to change was the location of the pear.ini file (option 11) so I did that first, changing it to C:\apps\php\PEAR\pear.ini. Then I changed the first option, and proceeded. This seems to work.
A whole bunch of stuff scrolls up the screen, too fast to react to, but it seems to be just warning about stuff, but otherwise working.
It pauses for input from me having noticed my php.ini doesn't have any PEAR settings in it, and giving me the option to fix that:
Would you like to alter the php.ini <C:\apps\php\5.4.14\php.ini> [Y/n] :
I agree.
It then lists some settings (one of them wrong), and asks me to ENTER to continue:
And I do so. It seems that's it, and I get prompted that I need to set some Windows environment variables in the registry, and PEAR has kindly created a registry fragment to do this. The fragment file is in PEAR/PEAR_ENV.reg, and has the following in it:
REGEDIT4
[HKEY_CURRENT_USER\Environment]
"PHP_PEAR_SYSCONF_DIR"="C:\\apps\\php\\PEAR\\"
"PHP_PEAR_INSTALL_DIR"="C:\\apps\\php\\PEAR\\\\pear"
"PHP_PEAR_DOC_DIR"="C:\\apps\\php\\PEAR\\\\docs"
"PHP_PEAR_BIN_DIR"="C:\\apps\\php\\PEAR\\"
"PHP_PEAR_DATA_DIR"="C:\\apps\\php\\PEAR\\\\data"
"PHP_PEAR_PHP_BIN"="C:\\apps\\php\\5.4.14\\php.exe"
"PHP_PEAR_TEST_DIR"="C:\\apps\\php\\PEAR\\\\tests"
that's all innocuous enough, so I run it.
The PEAR install has also added this to my php.ini, at the bottom:
;***** Added by go-pear
include_path=".;C:\apps\php\PEAR\\pear"
;*****
When I changed the install path earlier on, the install script got the path slightly wrong, adding two slashes at the end there. No matter what I tried whilst specifying the path would prevent this, so I guess it's a bug in the installer. I fix that path, and wonder about the registry stuff too. Sure enough it's messed those ones up to, so I fix that, and re-insert the registry fragment. This is probably overly pedantic of me, but it's nice to have these things right.
I now - possibly - have PEAR installed. I need to verify this. Back on the PEAR installation docs page there's a link to "Now check that PEAR works". This basically amounts to going into the PEAR directory in a command window, and running it:
Note that I have closed my previous command window and opened a new one (still as Administrator, just in case), so that those environment settings added by the registry fragment get picked up. It also asks me to confirm my version:
Step three of those instructions seems a bit of overkill (given step 4 will cover it by inference), but it gets one to enter some command-line PHP, which is quite neat so I did so. It suggests running this:
php -c /path/to/php.ini -r 'echo get_include_path()."\n";'
Which gave me an error:
PHP Parse error: syntax error, unexpected ''echo' (T_ENCAPSED_AND_WHITESPACE) i
n Command line code on line 1
I didn't get a clear answer from googling, but something I read made me try a simplified version, and this worked:
php -r "echo get_include_path();"
(I omitted the path to the config file given it's adjacent to php.exe, so assumed it would find it. It did).
This yields:
.;C:\apps\php\PEAR\pear
Which is correct.
So now I will create a PHP file which will verify PEAR is actually all working fine:
<?php
require_once 'System.php';
var_dump(class_exists('System', false));
?>
And this outputs:
bool(true)
Which is what I'm told to expect. Not very exciting though, is it?
OK, so actually installing PHPUnit now
Now that I have PEAR installed, I can use it. So the instructions to install PHPUnit via PEAR are these:pear config-set auto_discover 1
pear install pear.phpunit.de/PHPUnit
And doing that - which worked yesterday - gives me a big fat error today:
I then messed around with some instructions on the parent page,
PHPUnit PEAR Channel
Registering the channel:
pear channel-discover pear.phpunit.de
Listing available packages:pear remote-list -c phpunit
Installing a package:pear install phpunit/package_name
Installing a specific version/stability:pear install phpunit/package_name-1.0.0
pear install phpunit/package_name-beta
Receiving updates via a feed:http://pear.phpunit.de/feed.xml
C:\apps\php\PEAR>pear install phpunit
WARNING: "pear/PHPUnit" is deprecated in favor of "phpunit/PHPUnit"
Did not download dependencies: pear/PHP_Compat, use --alldeps or --onlyreqdeps t
o download automatically
pear/PHPUnit can optionally use package "pear/PHP_Compat"
downloading PHPUnit-1.3.2.tgz ...
Starting to download PHPUnit-1.3.2.tgz (20,913 bytes)
........done: 20,913 bytes
install ok: channel://pear.php.net/PHPUnit-1.3.2
C:\apps\php\PEAR>
Done. And checking in the PEAR directory for evidence of PHPUnit, I see C:\apps\php\PEAR\pear\PHPUnit in there, with a bunch of files. I presume this is what I am expecting.
Getting it to actually work
Nothing I tried from here worked. Then again, I still didn't really know what I was doing, and the docs go to great lengths to describe how to write tests, but I could not see anything anywhere explaining how to run the bloody things.After a bunch of googling I got the impression I should just be able to do this:
C:\apps\php\PEAR>phpunit C:\webroots\php\PHPUnit\StackTest.php
IE: there should be something runnable in the PEAR dir... however there was nothing executable in there. I began to wonder if that "no available releases for package "pear.phpunit.de/PHPUnit install failed" issue I thought I had worked around before hadn't actually worked.
I googled some more, and found an interesting article on StackOverflow covering much the same ground as I was experiencing, and this had two key steps I needed to undertake:
- pear update-channels
- pear clear-cache
C:\apps\php\PEAR>phpunit C:\webroots\php\PHPUnit\StackTest.php
PHPUnit 3.7.19 by Sebastian Bergmann.
.
Time: 0 seconds, Memory: 1.75Mb
OK (1 test, 5 assertions)
C:\apps\php\PEAR>
COOL!
NB: StackTest.php is the one from the docs:
<?php
class StackTest extends PHPUnit_Framework_TestCase
{
public function testPushAndPop()
{
$stack = array();
$this->assertEquals(0, count($stack));
array_push($stack, 'foo');
$this->assertEquals('foo', $stack[count($stack)-1]);
$this->assertEquals(1, count($stack));
$this->assertEquals('foo', array_pop($stack));
$this->assertEquals(0, count($stack));
}
}
?>
To make sure I was actually running that file, I added this as line 15:
$this->fail("nup");
Re-ran the tests and got this:
There was 1 failure:
1) StackTest::testPushAndPop
nup
C:\webroots\php\PHPUnit\StackTest.php:15
FAILURES!
Tests: 1, Assertions: 5, Failures: 1.
So yeah, it's working.
Revised steps for installing PHPUnit
In summary, after getting PEAR up and running, I needed to do this:pear config-set auto_discover 1
pear update-channels
pear clear-cache
pear install pear.phpunit.de/PHPUnit
Running Unit Tests in NetBeans
The command line is lovely, but I'd rather be able to run them from either the IDE itself, or from within a browser. Most of my colleagues run MXUnit tests from within Eclipse, but I like running a browser-based version, for some reason. Anyway. first things first... getting it running in NetBeans. More googling, this time for "phpunit netbeans". The first link takes me to some good instructions from Netbeans themselves.Firstly I need to install PHPUnit's Skeleton Generator. Sounds cool. Probably nothing to do with dem bones walkin' around though.
The install for the Skeleton Generator is familiar enough:
pear install phpunit/PHPUnit_SkeletonGenerator
And that burbles away with this lot:
(note that the install for PHPUnit looks much like that - although an awful lot more onscreen feedback - when it runs correctly. So look out for that when installing it).
To test this was working I created the test file as specified in the Skeleton install docs, thus:
<?php
class Calculator
{
public function add($a, $b)
{
return $a + $b;
}
}
?>
And after some trial and error, found this to be the command to generate the test skeleton:
C:\apps\php\PEAR>phpunit-skelgen --test -- Calculator C:\webroots\php\phpunit_ex
amples\Calculator.php
It's not clear from the docs example that that is what one needs to do, but the help for phpunit-skelgen clears it up:
C:\apps\php\PEAR>phpunit-skelgen
PHPUnit Skeleton Generator 1.2.0 by Sebastian Bergmann.
Usage: phpunit-skelgen --class ClassTest
phpunit-skelgen --class -- ClassTest [ClassTest.php] [Class] [Class.php]
phpunit-skelgen --test Class [Class.php] [ClassTest] [ClassTest.php]
phpunit-skelgen --test -- Class [Class.php] [ClassTest] [ClassTest.php]
--class Generate Class [in Class.php] based on ClassTest [in Class
Test.php]
--test Generate ClassTest [in ClassTest.php] based on Class [in C
lass.php]
--bootstrap <file> A "bootstrap" PHP file that is run at startup
--help Print this usage information
--version Print the version
C:\apps\php\PEAR>
Only the highlighted syntax would work for me. Note that one can also specify the name of both the test class and the file to put it in as well, but I wanted to see what the defaults were, so left it. Note the last line of the output when running the earlier phpunit-skelgen was this:
Wrote skeleton for "CalculatorTest" to "C:\webroots\php\phpunit_examples\CalculatorTest.php".
And indeed the file was created, thus:
<?php
/**
* Generated by PHPUnit_SkeletonGenerator 1.2.0 on 2013-05-06 at 15:50:34.
*/
class CalculatorTest extends PHPUnit_Framework_TestCase
{
/**
* @var Calculator
*/
protected $object;
/**
* Sets up the fixture, for example, opens a network connection.
* This method is called before a test is executed.
*/
protected function setUp()
{
$this->object = new Calculator;
}
/**
* Tears down the fixture, for example, closes a network connection.
* This method is called after a test is executed.
*/
protected function tearDown()
{
}
/**
* @covers Calculator::add
* @todo Implement testAdd().
*/
public function testAdd()
{
// Remove the following lines when you implement this test.
$this->markTestIncomplete(
'This test has not been implemented yet.'
);
}
}
That is pretty excellent. I like that. But this is all still command-line. How do I get it to work in Netbeans?
I go back to the instructions, and see it's a matter of adding these settings:
The PHPUnit Script: C:\apps\php\PEAR\phpunit.bat
And the Skeleton Generator Script: C:\apps\php\PEAR\phpunit-skelgen.bat
After doing that, I can right-click on Calculator.php in the Projects browser, select Tools > Create PHPUnit Tests. It asks me where to stick my tests (C:\webroots\php\phpunit_tests), and after some whirring it creates a new folder in the project "Test Files", and generates the CalculatorTest.php file within it:
I can then right-click on the CalculatorTest.php file and "Run" it, and the tests run. Now... yesterday when I was trying this, everything worked without further modification. However today I'm getting an error:
Fatal error: Class 'Calculator' not found in C:\webroots\php\phpunit_tests\phpunit_examples\CalculatorTest.php on line 18
Which is fair enough, as the test file has no way of knowing where Calculator.php is. So I adjust the file this:
require_once "../../phpunit_examples/Calculator.php";
And then the tests run A-OK. I guess they ran yesterday because I created the tests in the same directory as the files being tested, which I'd not normally do (and didn't do today).
I have to say though, I would have expected the "created your tests automatically" facility would be savvy enough to work out that the test files need to know where the files they're testing are! Not to worry. It could equally be some config I've messed up along the way.
But I still haven't got them working in the browser
I have some leads, but I'm a bit sick of all this now... I've been sitting here for the best part of five hours typing this in and googling about, and I'm out. I'll write a follow-up article on how to get it working in the browser too.I'm pretty pleased to have it working at all though... and both via command-line and IDE is definitely useful!
Stay tuned for further follow-up to this. I need a beer...
--
Adam