Tutorial
A short introduction to the test framework
PHPUnit provides a simple framework for creating a test suite to automate testing of functions and classes. PHPUnit is inspired by JUnit which was created by Kent Beck and Erich Gamma as a tool for eXtreme Programming. One of the rules of XP is to test small software components as often and early as possible, this way you will not have to fix bugs and errors in the API while setting up and testing larger applications which depend on the class. While unit testing is one of the fundimental rules in XP, you don't have to switch to XP to benefit from PHPUnit. PHPUnit stands alone as a good tool for testing classes or a set of functions and will ease your development cycle and help you to avoid endless debug sessions.
Work routine
Normally, you would write a class, do some unsystematic tests using echo() or var_dump(). After this, you use the class in your application and hope everything is ok. To benefit from PHPUnit you should rethink the flow. The best way is to do this:
-
1. design your class/API
-
2. create a test suite
-
3. implement the class/API
-
4. run the test suite
-
5. fix failures or errors and go to #4 again
It may seem that this will require a lot of time, but this impression is wrong. Creating the test suite using PHPUnit needs only a few minutes and running the test suite only seconds.
Design a class
Let's start with a small example: a string class. First we create a bunch of functions declarations to work on a string:
---- string.php ----
<?php
class String
{
//contains the internal data
var $data;
// constructor
function String($data) {
$this->data = $data;
}
// creates a deep copy of the string object
function copy() {
}
// adds another string object to this class
function add($string) {
}
// returns the formated string
function toString($format) {
}
}
?>
Creating test suite
Now we can create a test suite, which checks every function of your string class. A test suite is normal PHP class inherited from PHPUnit_TestCase containing test functions, identified by a leading 'test' in the function name. In the test function an expected value has to be compared with the result of the function to test. The result of this compare must delegate to a function of the assert*()-family, which decides if a function passes or fails the test.
---- testcase.php ----
<?php
require_once 'string.php';
require_once 'PHPUnit.php';
class StringTest extends PHPUnit_TestCase
{
// contains the object handle of the string class
var $abc;
// constructor of the test suite
function StringTest($name) {
$this->PHPUnit_TestCase($name);
}
// called before the test functions will be executed
// this function is defined in PHPUnit_TestCase and overwritten
// here
function setUp() {
// create a new instance of String with the
// string 'abc'
$this->abc = new String("abc");
}
// called after the test functions are executed
// this function is defined in PHPUnit_TestCase and overwritten
// here
function tearDown() {
// delete your instance
unset($this->abc);
}
// test the toString function
function testToString() {
$result = $this->abc->toString('contains %s');
$expected = 'contains abc';
$this->assertTrue($result == $expected);
}
// test the copy function
function testCopy() {
$abc2 = $this->abc->copy();
$this->assertEquals($abc2, $this->abc);
}
// test the add function
function testAdd() {
$abc2 = new String('123');
$this->abc->add($abc2);
$result = $this->abc->toString("%s");
$expected = "abc123";
$this->assertTrue($result == $expected);
}
}
?>
The first test run
Now, we can run a first test. Make sure that all the paths are correct and then execute this PHP program.
---- stringtest.php ----
<?php
require_once 'testcase.php';
require_once 'PHPUnit.php';
$suite = new PHPUnit_TestSuite("StringTest");
$result = PHPUnit::run($suite);
echo $result -> toString();
?>
If you call this script from the commandline, you will get the following output:
TestCase stringtest->testtostring() failed: expected true, actual false
TestCase stringtest->testcopy() failed: expected , actual Object
TestCase stringtest->testadd() failed: expected true, actual false
Every function fails the test, because your string functions didn't returned what we defined as the expected value.
If you want to call the script through your browser, you have to put the script in a correct html page and call $result->toHTML () instead of $result->toString().
Implementation
Ok, let's start with implementation of the our string class.
---- string.php ----
<?php
class String
{
//contains the internal data
var $data;
// constructor
function String($data) {
$this->data = $data;
}
// creates a deep copy of the string object
function copy() {
$ret = new String($this->data);
return $ret;
}
// adds another string object to this class
function add($string) {
$this->data = $this->data.$string->toString("%ss");
}
// returns the formated string
function toString($format) {
$ret = sprintf($format, $this->data);
return $ret;
}
}
?>
The implementation is complete and we can run the test again:
~>php -f stringtest.php
TestCase stringtest->testtostring() passed TestCase stringtest->testcopy() passed TestCase stringtest->testadd() failed: expected true, actual false
D'oh! the last test failed! We made a typing mistake. Change
line 16 in string.php
to
<?php
$this->data = $this->data.$string->toString("%s");
?>
and run the test again:
~>php -f stringtest.php
TestCase stringtest->testtostring() passed TestCase stringtest->testcopy() passed TestCase stringtest->testadd() passed
Everything is now OK!
Conclusion
Does it seem like a lot of work for testing three simple functions? Don't forget, this is a small example. Think about bigger, more complex API's like database abstraction or basket classes in a shop application. PHPUnit is an excellent tool to detect errors in the implementation of your class.
Often you will want to reimplement or refactor a large class which is used in several different applications. Without a test suite the likeliness of you breaking something in one of the applications that depends on your class is very high. Thanks to unit tests, you can create a test suite for your class, and then reimplement your class with the security of knowing that as long as the new class passes the tests, applications that depend on the class will work.