Quality Assurance on PHP projects - PHPUnit part 1
Of all tools available for improving quality assurance, there's one tool that is the core tool you have to master: PHPUnit. PHPUnit is a complete testing framework crafted by Sebastian Bergmann (@s_bergmann), who ported existing xUnit frameworks to PHP. And with this testing framework you're able to test your functionality in an automated way before you push code into production.
$this->_ttt->setGrid(new Grid());
$this->_ttt->setGrid(new Grid());
$this->_ttt->setGrid(new Grid());
// and see if we can win if we go diagonal right to left
[editorial] As I cannot summarize the whole usage of phpunit in one blogpost, this will be a sequence of several articles that will pick up a specific task you want to cover with phpunit.
Installation
Well, first of all you need to have PHPUnit installed on your system. The easiest way to accomplish this is to use the PEAR installer.
user@server: $ pear channel-discover pear.phpunit.de
user@server: $ pear channel-discover components.ez.no
user@server: $ pear channel-discover pear.symfony-project.com
Once you've got the right channels, you can install the framework:
user@server: $ pear install phpunit/PHPUnit
Configuration
You can use three methods to configure the way PHPUnit executes tests on your project:
- a configuration file
- command line parameters
- a combination of both
NOTE: I use phpunit with a configuration file that matches my project settings, so you might need to modify these settings if you're using a framework or have a different source code layout.
<phpunit bootstrap="./TestHelper.php" colors="true">
<testsuite name="Unit test suite">
<directory>./</directory>
</testsuite>
<filter>
<whitelist>
<directory suffix=".php">../src/</directory>
</whitelist>
</filter>
</phpunit>
This simple configuration file has 3 major parts:
1. The phpunit configuration tag
This tag starts the complete configuration, but also includes a bootstrap file (for bootstrapping your application like Zend Framework's bootstrapper class) and enables colors to show green, yellow and red colored bars to indicate the status of your tests. Since I'm a fan of a warm, fuzzy feeling whenever I see green, I enable this by default.
2. Testsuite section
The testsuite is a wrapper that bundles all your tests into a single suite, so you can call it immediately at execution. You can provide it with a name and a directory.
This section is very useful if you have multiple sections in your projects that you want to separate, as you can define the test suites based on their paths on the filesystem.
3. Filtering
Filtering allows you to quickly define what needs to be tested and what needs to be excluded. In my case I need just to investigate my Zend Framework application path and my custom library path. I also want to exclude all my templates as they don't contain any logic.
You can add also a section for logging where you can define code coverage reports (requires XDebug), a testdox progress report and other kinds of reports provided by PHPUnit. But since I will transfer these tasks to another tool, I'm not going to discuss it here.
I also use a bootstrap file for phpunit called TestHelper.php that allows me to "bootstrap" my test suite. This bootstrap file sets my defined constants, include paths, timezone and error reporting levels.
This is a very minimalistic bootstrapper:
<?php
// file: tests/TestHelper.php
// set our app paths and environments
define('BASE_PATH', realpath(dirname(__FILE__) . '/../'));
define('APPLICATION_PATH', BASE_PATH . '/src');
define('TEST_PATH', BASE_PATH . '/tests');
define('APPLICATION_ENV', 'testing');
// Include path
set_include_path(
'.'
. PATH_SEPARATOR . BASE_PATH . '/src'
. PATH_SEPARATOR . get_include_path()
);
// Set the default timezone !!!
date_default_timezone_set('Europe/Brussels');
// We wanna catch all errors en strict warnings
error_reporting(E_ALL|E_STRICT);
So how do you start?
Testing is really simple but for most people the complexity of having code testing other code and just report back if it's working or not is a bit confusing for people that haven't written any tests before.
For the most part, in my professional career I get a lot of excuses like "no time", "no budget" or "no interest". If it's truly such a big problem to cope with something as simple as writing a test, a career change might be in order. And I mean it. I believe that if you're serious about your job, you have to take a little responsibility in it. And writing tests is not that hard.
Let's begin with a classic example: a tic-tac-toe game. This is a simple pen-and-paper game for two players (O and X) that uses a grid of 3 by 3. The purpose of the game is to have a row, a column or a diagonal row filled with only O's or X's. More information can be found at http://en.wikipedia.org/wiki/Tic-tac-toe.
source: http://en.wikipedia.org/wiki/File:Tic-tac-toe-game-1.png
So how do you start with this kind of a game? Very simple: define the objects, the rules and the goal of this game in a list that makes sense to you. I like to define it as follows:
- objects:
- symbol X for user 1
- symbol Y for user 2
- play grid of 3 rows by 3 columns
- rules:
- write your symbol once for each turn within the grid
- goal:
- prevent opponent to win
- win by having 3 symbols on a single row, a single column or a single diagonal row
TictactoeTest
Let's define our general outline for our main class Tictactoe. We already know a few things: two players, a grid of 3 rows by 3 columns and a winner when three identical symbols are in a single row horizontal, vertical or diagonal.
In test code it would look something like this:
<?php
// file: tests/TictactoeTest.php
require_once 'Tictactoe.php';
class TictactoeTest extends PHPUnit_Framework_TestCase
{
public function testGameGridIsSetAtStart()
{
// test to see a grid is created of 3 by 3 and all fields are null
}
public function testGamePlayersAreSetAtStart()
{
// test to see players are set up at start with symbols X and O
}
public function testGameCanBePlayed()
{
// play a simple game with two players turn by turn
// returning true or false would notify us if there's a winner or not
}
}
We test 3 major things here: that a grid is set, that two players are created and that the game can be played. We don't worry for edge cases or exceptions now, as we can take care of that later.
When we run this test, we get a bunch of errors, basically saying that we don't have a Tictactoe class file yet. At this point, this is very normal.
user@server:/dev/ttt/tests $ phpunit
PHP Warning: require_once(Tictactoe.php): failed to open stream: No such file or directory in /dev/ttt/tests/TictactoeTest.php on line 3
Warning: require_once(Tictactoe.php): failed to open stream: No such file or directory in /dev/ttt/tests/TictactoeTest.php on line 3
PHP Fatal error: require_once(): Failed opening required 'Tictactoe.php' (include_path='.:/dev/ttt/src:.:/usr/lib/php/pear') in /dev/ttt/tests/TictactoeTest.php on line 3
Fatal error: require_once(): Failed opening required 'Tictactoe.php' (include_path='.:/dev/ttt/src:.:/usr/lib/php/pear') in /dev/ttt/tests/TictactoeTest.php on line 3
We've got are main goals set, now it's time to make some assertions to see we get the information we want.
First of all, we're going to setUp and tearDown our test case, so we know each test starts with a clean slate.
class TictactoeTest extends PHPUnit_Framework_TestCase
{
protected $_ttt;
protected function setUp()
{
parent::setUp();
$this->_ttt = new Tictactoe();
}
protected function tearDown()
{
$this->_ttt = null;
parent::tearDown();
}
Now we need to fill in the blanks regarding the test methods. Let's start with our first method:
public function testGameGridIsSetAtStart()
{
// we should be able to retrieve the grid from our Tictactoe class
$grid = $this->_ttt->getGrid();
// let's make sure we created a grid class to handle all grid related manipulations
$this->assertInstanceOf('Grid', $grid);
// we know the grid is 3 x 3, so let's count the rows
$this->assertEquals(3, count($grid->getRows()));
// and for each row, let's count the columns
// and since we're at it, let's check that their fields are empty
foreach ($grid->getRows() as $row) {
$this->assertInternalType('array', $row);
$this->assertEquals(3, count($row));
foreach ($row as $column) {
$this->assertNull($column);
}
}
}
So, what do we learn from this test? We need to have a Grid class to take care of all grid related things and we need to ensure each field is empty before we start playing. Take a note of this as we need it later!
The next step is to set up our players. We know we have two players and each player has his own symbol (X or O).
public function testGamePlayersAreSetAtStart()
{
// we should be able to retrieve all players from our Tictactoe class
$players = $this->_ttt->getPlayers();
// ensure that players have their own class to take care of all players
$this->assertInstanceOf('Players', $players);
// we know we only have two players
$this->assertEquals(2, count($players));
// we want to ensure that each player has the correct symbol
$this->assertEquals(Player::PLAYER_X, $players->seek(0)->current()->getSymbol());
$this->assertEquals(Player::PLAYER_O, $players->seek(1)->current()->getSymbol());
}
This test teaches us we need another two classes for our players. One class is acting as a collection implementing the SeekableIterator and Countable interfaces from SPL (I just love these, so I know I will use them!). The second class defines the player and the symbol it gets.
Our last test method is all about playing the game.
public function testGameCanBePlayed()
{
// let's pick just one player
$player = $this->_ttt->getPlayers()->seek(0)->current();
// and see if we can win if we created a row
$this->assertFalse($this->_ttt->play(0, 0, $player));
$this->assertFalse($this->_ttt->play(0, 1, $player));
$this->assertTrue($this->_ttt->play(0, 2, $player));
$this->_ttt->setGrid(new Grid());
// and see if we can win if we created a column
$this->assertFalse($this->_ttt->play(0, 1, $player));
$this->assertFalse($this->_ttt->play(1, 1, $player));
$this->assertTrue($this->_ttt->play(2, 1, $player));
$this->_ttt->setGrid(new Grid());
// and see if we can win if we go diagonal left to right
$this->assertFalse($this->_ttt->play(0, 0, $player));
$this->assertFalse($this->_ttt->play(1, 1, $player));
$this->assertTrue($this->_ttt->play(2, 2, $player));
$this->_ttt->setGrid(new Grid());
// and see if we can win if we go diagonal right to left
$this->assertFalse($this->_ttt->play(0, 0, $player));
$this->assertFalse($this->_ttt->play(1, 1, $player));
$this->assertTrue($this->_ttt->play(2, 2, $player));
}
In my next article we're going to set up our classes that should turn this test into a success. In the mean time, I challenge you to figure out how the code might look like. And if you want, share that code with everyone by posting it on pastebin.com, pastie.org or using github's gist.
Thank you. Thank you very much! Please don't stop. I want to see how to build an entire project from scratch using TDD (including handling views). I've never achieve this :(
ReplyDeleteThank you!