Unit testing is one of the practical facets of eXtreme programming. As a PHP developer just the concept on having some kind of system for testing may get the hacker in you screaming rebellion.
In this article I’ll look at a script which makes unit testing a pleasure rather than a burden.
But who cares if it works?
eXtreme programming, even by it’s name, is a controversial subject. Is it some kind of methodology for programming while sky diving?
Alot of what eXtreme programming advocates is “how to work” like coding in pairs. One aspect though which applies directly to code itself is unit testing.
In general the “testing lifecycle” of software is summarized as;
- unit testing: testing each “piece” of code
- system or integration testing: testing the code within the type of environment it will work in
- acceptance testing: testing the application from a user point of view
Now for your typical PHP project, testing is probably ad hoc, if at all, which is understandable with a language where you can build an “app” a user can begin working with in a matter of minutes.
But if the expiry date of the code we’re working on is longer than 24 hours, chances are we’ll end up writing something far bigger than we originally planned and start having to spend more and more time on debugging and testing code that wasn’t that well designed in the first place.
This is where unit testing can be a real help. The basic concept of unit testing is write more code which will test the main code we’ve written, by “throwing” sample data at it and examining what it gets back.
On reading “more code” you may be starting to lose interest but in general, writing test scripts is very little work and the good news is once they’re written, you can retest your code ad infinitum with nothing more than the click of a mouse. A change “here” may effect code we couldn’t have imagined - re-running a black box test tells us everything is still working properly.
There are two approaches to unit testing: black box testing (which is what we’ll be looking at here) and while box testing.
Black box testing takes the approach of working outside the code being tested (hence the code is a black box), passing data to it and examining what it gets back. This approach is particularily useful for classes where we’ve got a well designed API. It also means we don’t have to “mess” with the code we’re testing so generally less work.
White box testing takes the view that everything can ( and perhaps should ) be tested (no hiding behind APIs) and generally means adding the tests directly to the code being tested. In some cases it’s essential - a class method may return the correct value but some operation it performed “behind the scenes” has gone badly wrong. To some degree we’ve all used white box testing when using print statements inside a class to find out what’s going on.
In a perfect world, given infinite time and interest, we should probably use both to the fullest extent. Just using black box testing though is a good start and meets the practical constraint effecting most developers: lack of time.
One final piece of introduction: eXtreme programming suggests writing the tests before the code they’ll be testing.
From a developers perspectice, it takes some getting used to but, daring to intepret the intent of this practice, it’s meant to encourage developers to focus on designing a solid API. In general this has much in parallel with code that starts out as UML and isn’t such a far cry from what most developers probably do in their head anyway.
Unit Testing in Action
Enough theory. Now to make good the claim that unit testing (black box at least) is both easy and fast to implement.
There are quite a few PHP projects out there offering unit testing environments, which are all (hopefully) listed at the end of this article, all of which are projects approaching Java’s JUnit. PHP also has an assert() function which can be useful with procedural code but hamstrings itself by relying on a callback function to respond to exceptions.
In this article we’ll be using PHPUnit from Vincent OostindiĆ«, author of the Eclipse Library (which is well worth checking out by the way). It’s provided with the code for this article.
This particular PHPUnit is a single script which is lightweight and extremely easy to use. Some would argue this is exactly what a unit testing tool should be like.
Now a while back we looked at the Strategy Pattern.
Using Vincent’s PHPUnit (which is well documented by the way - just point your browser at it and all will be revealed), the first thing is to create a sub directory “test” which will contain all the test scripts and PHPUnit itself (in the same code it’s renamed to index.php for convienience).
Taking one class in particular to illustrate how the test scripts are written, lets test the ValidateUser class (from the strategy pattern article).
The basic “rules” of Vincents test environment are;
<list> Create one test class per test script, the class name being the same as the file name. All methods within the test class beginning with the letters “test” will be executed by PHPUnit All test classes must extend a class Test The test scripts need to include the script they will be testing (obviously).</list>
Have seen that, here’s a test script for the ValidateUser class;
<?php // This script is ValidateUserTest.php // Include the code to be tested require_once ('../lib/Validator.php'); // Tests the ValidateUser class class ValidateUserTest extends Test { var $validator; // Instance of class to test // Instantiate validator function ValidateUserTest() { // Instantiate the class to be tested $this->validator= new ValidateUser('username'); } // Test a valid username function testValidUser () { // Method should return true so test with Assert::equalsTrue Assert::equalsTrue($this->validator->isValid(), 'User is valid but returned isValid() false'); } // Test username containing invalid characters function testInvalidChars () { // Reset the errors $this->validator->errors=array(); // Set test data $this->validator->user='user%name'; // As this is called by a constructor, call it again $this->validator->validate(); // Method should return false so test with Assert::equalsFalse Assert::equalsFalse($this->validator->isValid(), 'User contains bad chars but isValid() returned true'); } // Test username that is too short function testInvalidShort () { // Reset the errors $this->validator->errors=array(); // Set test data $this->validator->user='user'; // As this is called by a constructor, call it again $this->validator->validate(); // Method should return false so test with Assert::equalsFalse Assert::equalsFalse($this->validator->isValid(), 'User is too short but isValid() returned true'); } // Test username that is too long function testInvalidLong () { // Reset the errors $this->validator->errors=array(); // Set test data $this->validator->user='usernameusernameusername'; // As this is called by a constructor, call it again $this->validator->validate(); // Method should return false so test with Assert::equalsFalse Assert::equalsFalse($this->validator->isValid(), 'User is too long but isValid() returned true'); } } ?>
Here’s what’s going on in the ValidateUserTest script;
First of all it’s stored in a file with the same name as the class (this is important or PHPUnit will ignore it).
Next we include the script that will be tested (Validator.php), so that the test script can call it’s code.
The test class itself is defined as a subclass of Test (part of PHPUnit). The Test class is declared within PHPUnit so there’s no need to include any files here.
In the constructor of ValidateUserTest we instantiate the class that is being tested and assign it to a local member variable. This makes it available to all test methods to operate on.
PHPUnit will now execute each method in ValidateUserTest that beings with the word test, in the order they appear within the class.
Taking the first example;
// Test a valid username
function testValidUser () {
// Method should return true so test with Assert::equalsTrue
Assert::equalsTrue($this->validator->isValid(),
'User is valid but returned isValid() false');
}
The Assert::equalsTrue() method takes a boolean value as it’s first parameter and a message to display if the first parameter is not TRUE. In other words, given that we’ve constructed ValidateUser with a username we expect to be valid, this test should return true. If not it will generate a warning message.
The next test uses the Assert::equalsFalse;
// Test username containing invalid characters
function testInvalidChars () {
// Reset the errors
$this->validator->errors=array();
// Set test data
$this->validator->user='user%name';
// As this is called by a constructor, call it again
$this->validator->validate();
// Method should return false so test with Assert::equalsFalse
Assert::equalsFalse($this->validator->isValid(),
'User contains bad chars but isValid() returned true');
}
First note that we have to “re-build” the expected environment for the ValidateUser class, by resetting the errors array and assigning a new user name to check.
Note: In retrospect it would probably have been better to instantiate a new object from ValidateUser for this test method, rather than assigning values to class variables directly, but I had to allow the hacker in me some license...
The Assert::equalsFalse() method is basically the opposite of the Assert::equalsTrue() method - if it doesn’t get given a FALSE value as it’s first parameter, it well display the error message.
The other test methods are variations on what we’ve already seen.
The reasons why these tests were selected was to test that ValidateUser is doing it’s validation job correctly. The test data, namely the values “username” (a valid user name), “user%name” (contains an invalid character), “user” (too short), “usernameusernameusername” (too long) represent sample data for the cases we expect to have to deal with. Of course the more test data we use, the better: a divide by zero error (for example) will only show up if we pass a zero to a method, so only testing with values greater than 0 doesn’t help.
Now executing PHPUnit, we have to provide the full (filesystem not URL) path to the directory containing the test scripts. And here’s what we get back;
2) Aaargh! Panic - a problem. In the base Validator class the method;
function isValid () {
if ( isset ($this->errorMsg) ) {
return false;
} else {
return true;
}
}
isset()? Turns out after writing that method, I started setting $this→errorMsg as an array in the constructor of Validator - code that had worked now no longer worked. If I’d been using PHPUnit sooner I’d have spotted it.
The fix;
function isValid () {
if ( count ($this->errorMsg) > 0 ) {
return false;
} else {
return true;
}
}
One final point - with PHP we could (at least in theory) perform white box testing without needing to modify the code your testing itself, either using eval() or the tokenizer extension. Interpreted languages do have their advantages...
Anyway, that’s a fast tour of unit testing. The test scripts for this example took about 20 minutes to write (for all the Validator classes). The pay off is hours of time saved on useless debugging. Convinced?
Resources
Thinking in Patterns with Java - Bruce Eckel provides useful practical insight on unit testing in Java [ free to download ] Effortless (or Better!) Bug Detection with PHP Assertions thought provoking article on unit testing using PHP‘s assert() function. PEAR::PHPUnit Tutorial
SimpleTest - fully featured test framework including Mock Objects.
A variety of other PHP Unit testers ( all called phpunit !!! ) PHPUnit [pear] by Sebastian Bergmann PhpUnit [sourceforge] by Fred Yankowski phpUnit [also sourceforge] by Stuart Herbert
