Category: PHP
Become a better PHP programmer
- with PHPUnit
I attend on different forums and lists all over the internet, and I often see good PHP programmers giving out great solutions. But when there's a question about a higher knowledge of projects and maybe not direclty programming related, there's not as many answers as there usually is. In this article I will try to show you the beauty of developing a PHP project with Test Driven Development and PHPUnit for the unit testing.
First of all; All PHP development machines should have PEAR installed. It's easy to access good development libraries, and one of those libraries will be needed when following this article. Installation notes for PEAR are available at http://pear.php.net/manual/en/installation.php.
PHPUnit and Test Driven Development
PHPUnit is a unit testing suite for PHP, with counterparts in many languages - like JUnit for Java, NUnit for .NET, HUnit for Haskell, among others. PHPUnit can be downloaded easily through PEAR of course, but they are also available online. With PHPUnit we can write test cases, run the tests in a defined suite and watch the results of the test - either in a browser or in a commandline.
Test Driven Development, or TDD, is a design pattern with a short development cycle. The basics are that you start out with writing your test cases and defining the functionality of your class. Of course this first test will fail, since you haven't actually implemented any functionality. Now you start to implement this functionality in your class until the test case passes, then you write more tests, implement some more functionality, and so on - until you have the functionality of the class implemented. This (often) ends up in good looking, refactored code as well.
In my example, I will define a AutomatedCashier which should read groceries from an array and sum up the value of these groceries.
First of all, we need a simple POPO (Plain Old PHP Object) - an entity to keep data for exactly one Grocery. Lets start out by creating an empty Grocery class.
<?php
class Grocery
{
/* Empty for now */
}
Now we start by setting up the test for the Grocery class, we call it GroceryTest (in the file GroceryTest.php), and we need to include the Grocery classfile:
<?php
require_once "Grocery.php";
class GroceryTest extends PHPUnit_Framework_TestCase
{
/* Of course we will need an instance of
our Grocery class, so we can perform
tests on it */
protected $grocery;
/* The initializing method, this is where
we initialize our class for testing */
protected function setUp()
{
$this->grocery = new Grocery();
}
/* The deallocating method, this is where
we deallocate our instance of Grocery */
protected function tearDown()
{
unset($this->grocery);
}
}
That's the basic a test case can be, thou it will not perform any tests for us yet. We know that every grocery should have a price, so lets implement a test case for grocery prices. Add the following method to GroceryTest:
...
/* Every test case method should begin with test*
*/
public function testGetPrice()
{
$groceryPrice = 10;
$this->assertEquals($this->grocery->getPrice(), $groceryPrice);
}
...
Now it's time to test our file, start up a terminal and go to the location of your newly created PHP files. Run the 'phpunit' command;
$ phpunit --verbose GroceryTest
The verbose parameter tells PHPUnit to yield the output to the terminal. Of course this first test wouldn't complete successfully, since we haven't implemented the getPrice() method of Grocery entity. Lets implement it:
<?php
class Grocery
{
private $price;
/* Getter method for the price of the Grocery */
public function getPrice()
{
return $this->price;
}
}
Run the test again, and you should get a different output. This time PHPUnit finds the method, but we get a failure saying "Failed asserting that
Well, no surprise there. We have not provided our Grocery with a price. Lets set up the Grocery class to recieve a price when it's initialized - i.e. add a constructor to it:
<?php
class Grocery
{
private $price;
public function __construct($price)
{
$this->price = $price;
}
...
}
Now we also need to tell GroceryTest to assign a price to the Grocery instance, and we do that in setUp():
public function setUp()
{
$this->grocery = new Grocery(10);
}
If we run the test again, PHPUnit will tell us "OK (1 test)". Every Grocery should also have a name and if it's sold by unit or by weight. Write test cases and implement the needed methods.
My complete GroceryTest class ended up like this;
<?php
require_once "Grocery.php";
class GroceryTest extends PHPUnit_Framework_TestCase
{
/* We need an instance variable holding the current
* instance to the Grocery */
protected $grocery;
/* The initializing method, this is where
* we initialize our class for testing */
protected function setUp()
{
$name = "Onions";
$price = 10;
$type = Grocery::SOLD_BY_WEIGHT;
$this->grocery = new Grocery($name, $price, $type);
}
/* Test for getPrice() method */
public function testGetPrice()
{
$groceryPrice = 10;
$this->assertEquals($this->grocery->getPrice(), $groceryPrice);
}
/* Test for getName method */
public function testGetName()
{
$groceryName = "Onions";
$this->assertEquals($this->grocery->getName(), $groceryName);
}
/* Test to see if our Grocery is sold by weight or by unit */
public function testGetType()
{
$this->assertTrue($this->grocery->isSoldByWeight());
}
/* The deallocating method, this is where
* we deallocate our class */
protected function tearDown()
{
unset($this->grocery);
}
}
And my complete Grocery class looks like this;
<?php
class Grocery
{
/* Every grocery has a price */
private $price;
/* Every grocery has a name */
private $name;
/* The grocery can be sold by weight or by unit,
* default is by unit */
private $type = self::SOLD_BY_UNIT;
/* Constants to define a grocery by
* weight or by unit */
const SOLD_BY_UNIT = 0x00;
const SOLD_BY_WEIGHT = 0x01;
/* The constructor which initializes the entity,
* as well as some getters for the data flow */
public function __construct($name, $price, $type = self::SOLD_BY_UNIT)
{
$this->name = $name;
$this->price = $price;
$this->type = ($type == self::SOLD_BY_WEIGHT)
? self::SOLD_BY_WEIGHT
: self::SOLD_BY_UNIT;
}
public function getName()
{
return $this->name;
}
public function getPrice()
{
return $this->price;
}
public function isSoldByUnit()
{
return (self::SOLD_BY_UNIT == $this->type);
}
public function isSoldByWeight()
{
return (self::SOLD_BY_WEIGHT == $this->type);
}
}
OK, first part's done. As you can see from the test case, PHPUnit provides assertion methods for testing. In my test case above, I used assertEquals() and assertTrue(). assertEquals() takes two parameters, $actual and $expected, and checks for equality. assertTrue() on the other hand, takes only one parameter - a boolean condition. PHPUnit provides not only invers of these to (assertNotEquals() and assertFalse()), but it provides 31 different assertion methods at the moment. You can find the full listing here.
Time to get our Cashier running, the store is full of Groceries and customers!
We continue our unit testing marathon by creating the actual class for AutomatedCashier:
<?php
require_once "Grocery.php";
class AutomatedCashier
{
/* Empty for now */
}
And of course our test case:
<?php
require_once "Grocery.php";
require_once "AutomatedCashier.php";
class AutomatedCashierTest extends PHPUnit_Framework_TestCase
{
/* Empty for now */
}
OK, let the coding begin. Firstly we know that an AutomatedCashier won't work unless it's given some groceries and our test case wont work without an AutomatedCashier, so we write a test for adding groceries as well as providing a cashier for the test case;
...
class AutomatedCashierTest extends PHPUnit_Framework_TestCase
{
protected $cashier;
protected function setUp()
{
$this->cashier = new AutomatedCashier();
}
public function testAddGrocery()
{
$grocery = new Grocery("Onions", 1.19, Grocery::SOLD_BY_WEIGHT);
$this->cashier->addGrocery($grocery);
$this->assertTrue($this->cashier->hasGroceries());
}
protected function tearDown()
{
unset($this->cashier);
}
}
Lets implement the addGrocery() and hasGroceries() method as well;
...
class AutomatedCashier
{
public function addGrocery(Grocery $grocery)
{
//??
}
public function hasGroceries()
{
}
}
Lets run the unit test;
$ phpunit --verbose AutomatedCashierTest
OK, no surprise there. We haven't added functionality to our methods in AutomatedCashier. Lets do that;
class AutomatedCashier
{
private $groceries;
public function addGrocery(Grocery $grocery)
{
$this->groceries[] = $grocery;
}
public function hasGroceries()
{
return (sizeof($this->groceries) > 0);
}
}
Notice how I make use of type hinting in the argument list of addGrocery(). This restricts other types than Grocery to be added to the list. Run the test again, and you should get an "OK (1 test)".
Lets try with adding more than one Grocery to the cashier;
...
public function testAddGroceries()
{
$this->cashier->addGrocery(
new Grocery("Onions", 1.19, Grocery::SOLD_BY_WEIGHT)
);
$this->cashier->addGrocery(
new Grocery("Pizza", 6.59, Grocery::SOLD_BY_UNIT)
);
$this->cashier->addGrocery(
new Grocery("Soda", 1.99, Grocery::SOLD_BY_UNIT)
);
$this->cashier->addGrocery(
new Grocery("Tomatoes", 2.49, Grocery::SOLD_WEIGHT)
);
$this->assertTrue($this->cashier->numberOfGroceries() == 4);
}
Gha, to much to write! Lets create a method in AutomatedCashier that can take more than one Grocery;
...
public function addGroceries(array $groceries)
{
foreach ($groceries as $grocery) {
$this->addGrocery($grocery);
}
}
Notice the type hinting! Lets rewrite our testAddGroceries() method in the test case to use the newly created method:
...
public function testAddGroceries()
{
$groceries = array(
new Grocery("Onions", 1.19, Grocery::SOLD_BY_WEIGHT),
new Grocery("Pizza", 6.59, Grocery::SOLD_BY_UNIT),
new Grocery("Soda", 1.99, Grocery::SOLD_BY_UNIT),
new Grocery("Tomatoes", 2.49, Grocery::SOLD_BY_WEIGHT)
);
$this->cashier->addGroceries($groceries);
$this->assertTrue($this->cashier->numberOfGroceries() == sizeof($groceries));
}
That looks better! Run the test now and we'll get a "Fatal error: Call to undefined method AutomatedCashier::numberOfGroceries()...". No surprise! Lets implement it in AutomatedCashier;
...
public function numberOfGroceries()
{
return sizeof($this->groceries);
}
The test passes (along with our first testAddGrocery()). Lets add a similar test, where we don't only add groceries - we add bogus data as well:
...
/**
* @expectedException PHPUnit_Framework_Error
*/
public function testAddNonGroceries()
{
$groceries = array(
new Grocery("Pizza", 6.59, Grocery::SOLD_BY_UNIT),
"bogus!",
0xCAFEBABE,
new Grocery("Tomatoes", 2.49, Grocery::SOLD_BY_WEIGHT)
);
try {
$this->cashier->addGroceries($groceries);
} catch(Exception $e) {
return;
}
$this->fail("Expected exception not raised in " . __METHOD__);
}
Now we're on deep water. The comment block above the method is called an annotation. Annotaions can be very useful when testing, there's a bunch of them available through PHPUnit. The one I'm using is expectedException - that means that our test method should raise an exception. Remember the type hinting in the addGrocery() method of AutomatedCashier? This restricts other types than a Grocery to be accepted as an argument. If any other type comes as an argument PHP will raise an error, which is converted into a PHPUnit_Framework_Error (notice the annotation) as an exception - which in turn we catch in the try-catch block. If we catch it everything is fine, so we just return from the method. If nothing is catched - that's when we should start worrying! Run the test, and you should get the output "OK (3 tests)".
Now what? Our cashier can recieve groceries and stores them, but where to go from now? The natural step is that the cashier sums up the price of the groceries and tells us that. Lets write a test case!
public function testGetReciptSum()
{
$grocieries = array(
new Grocery("Onions", 1.19, Grocery::SOLD_BY_WEIGHT),
new Grocery("Pizza", 6.59, Grocery::SOLD_BY_UNIT),
new Grocery("Soda", 1.99, Grocery::SOLD_BY_UNIT),
new Grocery("Tomatoes", 2.49, Grocery::SOLD_BY_WEIGHT)
);
$totalSum = 1.19 + 6.59 + 1.99 + 2.49;
$this->cashier->addGroceries($groceries);
$this->assertTrue($totalSum == $this->cashier->getReciptSum());
}
Time to implement AutomatedCashier::getReciptSum();
public function getReciptSum()
{
$reciptSum = 0;
foreach ($this->groceries as $grocery) {
$reciptSum += $grocery->getPrice();
}
return $reciptSum;
}
Run the tests again and all tests should pass.
That's about all for this article, we've created an AutomatedCashier which can handle groceries. You could continue with TDD of the AutomatedCashier - implementing methods for printing neat PDFs of the recipt or setting up payment methods, as excercise. Hopefully this article gave you an insight of how to work with PHPUnit and how this can increase your programming skills, performance and development speed.
Björn Wikström, 2009-11-24