It's 3 a.m. Do you know if your code is still working?
Web applications run 24x7, and wondering whether my application is still running has kept me up at night. Unit testing has helped me gain significant confidence in my code -- and get a good night's sleep.
Unit tests are a framework for writing tests on code and running those tests automatically. Test-driven development is a unit test methodology that says that you first write the test and verify that tests find the errors, then write the code required for the tests to pass. When all the tests pass, the feature you're developing should be complete. The value of these unit tests is that you can run them at any time -- before code is checked in, after significant refactoring, or after deployment to a running system.
For PHP, the unit test framework is PHPUnit2. You can install the system as a PEAR module using the PEAR command line: % pear install PHPUnit2.
After you've installed the framework, you can begin writing unit tests by creating test classes that derive from PHPUnit2_Framework_TestCase.
I've found that the best place to begin unit testing is with the application's business logic modules. I use a simple example: a function that adds two numbers. To start testing, I write the tests first, as shown below.
Listing 1. TestAdd.php
<?php
require_once 'Add.php';
require_once 'PHPUnit2/Framework/TestCase.php';
class TestAdd extends PHPUnit2_Framework_TestCase
{
function test1() { $this->assertTrue( add( 1, 2 ) == 3 ); }
function test2() { $this->assertTrue( add( 1, 1 ) == 2 ); }
}
?>
|
This TestAdd class has two methods in it, both with the prefix test. Each method defines a test, and that test can be as simple as Listing 1 or as complex as you need it to be to fully test some aspect of the feature you're developing. In this case, I'm simply asserting that one plus two equals three in the first test and one plus one equals two in the second test.
The PHPUnit2 system defines the assertTrue() method, which is used to test that the conditional held in the argument evaluates to true. Then, I write the Add.php module, which implements enough of the code that the tests initially fail.
Listing 2. Add.php
<?php
function add( $a, $b ) { return 0; }
?>
|
When I run the unit tests now, both tests fail.
Listing 3. Tests fail
% phpunit TestAdd.php PHPUnit 2.2.1 by Sebastian Bergmann. FF Time: 0.0031270980834961 There were 2 failures: 1) test1(TestAdd) 2) test2(TestAdd) FAILURES!!! Tests run: 2, Failures: 2, Errors: 0, Incomplete Tests: 0. |
Now I know both tests work. So, I can modify the add() function to actually do the right thing.
<?php
function add( $a, $b ) { return $a+$b; }
?>
|
And both tests now pass.
Listing 4. Tests pass
% phpunit TestAdd.php PHPUnit 2.2.1 by Sebastian Bergmann. .. Time: 0.0023679733276367 OK (2 tests) % |
This example of test-driven development is quite simple, but you get the point. You first create the tests and enough of the code to get the test running, but failing. Then you verify that the test fails and implement the code to get it to pass.
I find that as I implement the code, I end up adding more tests until I have a complete test set that checks all the variants in the code paths. You can find suggestions on what tests to write and how to write them at the end of this article.
After module testing, you test database access. Database access testing brings up a couple of interesting issues. First, you must reset the database to some known point before each test. Second, be aware that this reset could cause database damage to live databases, so you must test against a database that is different from your production database or write the tests so that they don't affect the existing database content.
Database unit tests begin with the database. To illustrate that, I need the simple schema shown below.
Listing 5. Schema.sql
DROP TABLE IF EXISTS authors; CREATE TABLE authors ( id MEDIUMINT NOT NULL AUTO_INCREMENT, name TEXT NOT NULL, PRIMARY KEY ( id ) ); |
Listing 5 is a table of authors, each with an associated ID.
Next, I write the tests.
Listing 6. TestAuthors.php
<?php
require_once 'dblib.php';
require_once 'PHPUnit2/Framework/TestCase.php';
class TestAuthors extends PHPUnit2_Framework_TestCase
{
function test_delete_all() {
$this->assertTrue( Authors::delete_all() );
}
function test_insert() {
$this->assertTrue( Authors::delete_all() );
$this->assertTrue( Authors::insert( 'Jack' ) );
}
function test_insert_and_get() {
$this->assertTrue( Authors::delete_all() );
$this->assertTrue( Authors::insert( 'Jack' ) );
$this->assertTrue( Authors::insert( 'Joe' ) );
$found = Authors::get_all();
$this->assertTrue( $found != null );
$this->assertTrue( count( $found ) == 2 );
}
}
?>
|
This set of tests covers deleting the authors from a table, inserting authors into a table, and inserting authors while verifying that they're there. This is an additive cascade of tests I find convenient for finding errors. I can quickly tell what is failing from seeing which tests worked and which tests didn't, then understanding the differences between them.
The initial failing version of the dblib.php PHP database access code is shown below.
Listing 7. Dblib.php
<?php
require_once('DB.php');
class Authors
{
public static function get_db()
{
$dsn = 'mysql://root:password@localhost/unitdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
return $db;
}
public static function delete_all()
{
return false;
}
public static function insert( $name )
{
return false;
}
public static function get_all()
{
return null;
}
}
?>
|
Running the unit tests on the code in Listing 8 shows that all three tests fail:
Listing 8. Dblib.php
% phpunit TestAuthors.php PHPUnit 2.2.1 by Sebastian Bergmann. FFF Time: 0.007500171661377 There were 3 failures: 1) test_delete_all(TestAuthors) 2) test_insert(TestAuthors) 3) test_insert_and_get(TestAuthors) FAILURES!!! Tests run: 3, Failures: 3, Errors: 0, Incomplete Tests: 0. % |
Now I can add the code -- method by method -- that will access the database properly until all three tests pass. The final version of the dblib.php code is shown below.
Listing 9. The completed dblib.php
<?php
require_once('DB.php');
class Authors
{
public static function get_db()
{
$dsn = 'mysql://root:password@localhost/unitdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
return $db;
}
public static function delete_all()
{
$db = Authors::get_db();
$sth = $db->prepare( 'DELETE FROM authors' );
$db->execute( $sth );
return true;
}
public static function insert( $name )
{
$db = Authors::get_db();
$sth = $db->prepare( 'INSERT INTO authors VALUES (null,?)' );
$db->execute( $sth, array( $name ) );
return true;
}
public static function get_all()
{
$db = Authors::get_db();
$res = $db->query( "SELECT * FROM authors" );
$rows = array();
while( $res->fetchInto( $row ) ) { $rows []= $row; }
return $rows;
}
}
?>
|
When I run the tests on this code, all the tests run without error, and I know that my code is working properly.
The next step in testing your whole PHP application is to test the Hypertext Markup Language (HTML) interface on the front end. To do that, you need a Web page like the one shown below.
Figure 1. Test Web page
This page adds two numbers. To test the page, begin with the unit test code.
Listing 10. TestPage.php
<?php
require_once 'HTTP/Client.php';
require_once 'PHPUnit2/Framework/TestCase.php';
class TestPage extends PHPUnit2_Framework_TestCase
{
function get_page( $url )
{
$client = new HTTP_Client();
$client->get( $url );
$resp = $client->currentResponse();
return $resp['body'];
}
function test_get()
{
$page = TestPage::get_page( 'http://localhost/unit/add.php' );
$this->assertTrue( strlen( $page ) > 0 );
$this->assertTrue( preg_match( '/<html>/', $page ) == 1 );
}
function test_add()
{
$page = TestPage::get_page( 'http://localhost/unit/add.php?a=10&b=20' );
$this->assertTrue( strlen( $page ) > 0 );
$this->assertTrue( preg_match( '/<html>/', $page ) == 1 );
preg_match( '/<span id="result">(.*?)<\/span>/', $page, $out );
$this->assertTrue( $out[1]=='30' );
}
}
?>
|
This test uses the HTTP Client module from PEAR. I find it a bit easier than the built-in PHP Client URL Library (CURL), although you could use that one, too.
One test checks the page for a return and determines whether the page contains HTML. The second test asks for the sum of 10 and 20 by putting those values in the URL of the request, then checking the result span encoded in the page.
The code for the page is shown below.
Listing 11. TestPage.php
<html><body><form> <input type="text" name="a" value="<?php echo($_REQUEST['a']); ?>" /> + <input type="text" name="b" value="<?php echo($_REQUEST['b']); ?>" /> = <span id="result"><?php echo($_REQUEST['a']+$_REQUEST['b']); ?></span> <br/> <input type="submit" value="Add" /> </form></body></html> |
This page is quite simple. Two input fields show the current values from the request. The result span shows the sum of those two values. The <span> tag makes all the difference: It's invisible to the user, but not to the unit test. So the unit test doesn't require complex tree logic to find the value. Instead, it retrieves the value of a particular <span> tag. This way, when the interface changes, the test will pass as long as the span is there.
As before, you first code the test, then create a failing version of the page. You test the failure, then change the page so it works. Here's the result:
Listing 12. Test the failure, then change the page
% phpunit TestPage.php PHPUnit 2.2.1 by Sebastian Bergmann. .. Time: 0.25711488723755 OK (2 tests) % |
Both tests pass, which means that the code works properly.
There is a catch to testing the HTML front end, though: the JavaScript. The Hypertext Transport Protocol (HTTP) client code retrieves the page, but doesn't execute the JavaScript. So if you have a lot of code in your JavaScript file, you must create a User Agent-level unit test. I've found that the best way to do that is to use the automation layer built into Microsoft® Internet Explorer®. Microsoft Windows® scripts written in PHP can use the Component Object Model (COM) interface to control Internet Explorer, have it navigate to pages, then use Document Object Model (DOM) methods to find out what the elements of the page look like after certain user operations.
That is the only way I've found to unit test JavaScript code on the front end. And I readily admit it's not easy to write or maintain and that those types of tests are prone to breakage as you make minor modifications to the pages.
What tests to write and how to write them
When I'm writing tests, I like to cover the following conditions:
- All positive tests
- This set of tests ensures that everything works as expected.
- All failure tests
- Use these tests on a one-by-one basis to ensure that every failure or exception case works.
- Positive sequence tests
- This set of tests ensures that calls in the correct order work as expected.
- Negative sequence tests
- This set of tests ensures that when calls are made out of order, they fail.
- Load tests
- When appropriate, you can perform a small set of tests to determine that the performance of those tests is within an expected range. For example: 2,000 calls should be processed within two seconds.
- Resource tests
- These tests ensure that the application program interface (API) properly allocates and frees resources -- for example, opening, writing, and closing a file-based API several times in a row to ensure that no files remain open.
- Callback tests
- For APIs that have callback methods, these tests ensure that the code runs properly if callbacks are not defined. In addition, these tests ensure that the code runs properly when callbacks are defined but behave inappropriately or generate exceptions.
These are a few ideas for your unit tests. I also have suggestions about how to write your unit tests:
- No random data
- Although it may seem like a good idea to throw random data at an interface, try to avoid it because the data is hard to debug. If data is generated randomly on each invocation, you may get an error on one pass that you don't get on another. If your test requires random data, generate the data in a file, then use that file on every run. In this way, you can have "noisy" data, but still be able to debug errors.
- Group your tests
- It's easy to go overboard and have thousands of tests that take hours to run completely. That's fine, but group those tests so you can run one quick set to check the basics, then have the full set run overnight.
- Write strong APIs and strong tests
- It's important to write your APIs and your tests so that they aren't easily broken as you add new functionality or modify existing functionality. There is no universal silver bullet here, but suffice it to say that a squeaky test (one that gyrates from pass to fail and back regularly) soon gets dropped.
Unit tests are valuable for engineers. They are one of the cornerstones of the agile development process -- a process that emphasizes code, as the documentation requires some proof that code is working to the specification. Unit tests provide that proof. The process starts with the unit tests defining what the code should, but currently doesn't, do. So, all the tests start off as failing. Then, as the code nears completion, the tests begin to pass. When all the tests pass, the code is complete.
I never write large pieces of code, or refactor large or complex code blocks without unit tests. I often find myself writing unit tests to existing code before modifying the code to ensure that I know what I'm breaking (or not breaking) as I make changes. That assurance gives me great confidence that the code I ship to customers is running properly -- even at 3 a.m.
Learn
-
PHP.net is an excellent resource for PHP coders.
-
The PHPUnit2 module framework was used in this article.
-
Books like Kent Beck's Test-Driven Development provide insight into this new method of programming based on testing.
-
The C2 wiki has good information about unit tests.
-
The PHPUnit Pocket Guide, by Sebastian Bergmann, is a good short book on the PHPUnit framework.
-
The PHP Extension and Application Repository is an excellent resource for PHP developers.
-
Visit IBM developerWorks' PHP project resources to learn more about PHP.
-
Browse all of the PHP content on developerWorks.
-
Stay current with developerWorks technical events and webcasts.
-
Check out upcoming conferences, trade shows, webcasts, and other Events around the world that are of interest to IBM open source developers.
-
Visit the developerWorks Open source zone for extensive how-to information, tools, and project updates to help you develop with open source technologies and use them with IBM's products.
-
To listen to interesting interviews and discussions for software developers, be sure to check out developerWorks podcasts.
Get products and technologies
-
Innovate your next open source development project with IBM trial software, available for download or on DVD.
Discuss
-
Get involved in the developerWorks community by participating in developerWorks blogs.
Comments (Undergoing maintenance)





