Skip to main content

Quality Assurance on PHP projects - PHPUnit part 3


Time for the third part on unit testing with phpunit in my Quality Assurance on PHP projects series. In part one we started writing unit tests for a simple game of tic-tac-toe. In part two we started converting our unit tests into actual code and moved our general unit test code for grids into a Grid focussed unit test. In this part, we're looking at how we can optimize the tests for our players.

When we look at our initial tests for players, we only focussed on two things: each player has a symbol and players are managed within a collection class.
    public function testGamePlayersAreSetAtStart()
    {
        $players = $this->_ttt->getPlayers();
        $this->assertInstanceOf('Players', $players);
        $this->assertEquals(2, count($players));
        $this->assertEquals(Player::PLAYER_X, $players->seek(0)->current()->getSymbol());
        $this->assertEquals(Player::PLAYER_O, $players->seek(1)->current()->getSymbol());
    }
As you can imagine, this is a very minimalistic test that cannot cope with all player related issues. We did have two classes to handle all player related issues and thus we need to move our test out of our TictactoeTest class.

A quick look at the functionality of a player should give us more fuel to creat better unit tests. Bytaking a new look at the functionality of a single player, we can optimize our tests. Since we already have some working code, let's take a quick look at the Player class to figure out what is being expected.
<?php
/**
 * TicTacToe
 * 
 * A simple game that's played with two players, each taking a turn by marking
 * a field in a grid of 3 x 3 with either an X or an O (one symbol per player).
 * Winner is the one who has 3 identical symbols in a single horizontal,
 * vertical or diagonal row.
 * 
 * @package Tictactoe
 * @license "Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)"
 * @link http://creativecommons.org/licenses/by-sa/3.0/
 */
/**
 * Player
 * 
 * This Player class is responsible to set up and maintain a single player
 * 
 * @package Tictactoe
 * @category Tictactoe
 *
 */
class Player
{
    /**
     * Defines the symbol X for a player to choose
     * 
     * @var string
     */
    const PLAYER_X = 'X';
    /**
     * Defines the symbol X for a player to choose
     * 
     * @var string
     */
    const PLAYER_O = 'O';
    /**
     * The symbol a player chooses
     * 
     * @var string
     */
    protected $_symbol;
    /**
     * Constructor for a single player, with an optional symbol (X or O) to
     * define the player's playing symbol
     * 
     * @param string $symbol
     */
    public function __construct($symbol = null)
    {
        if (null !== $symbol) {
            $this->setSymbol($symbol);
        }
    }
    /**
     * Sets a symbol for this Player
     * 
     * @param string $symbol
     * @return Player
     */
    public function setSymbol($symbol)
    {
        $this->_symbol = (string) $symbol;
        return $this;
    }
    /**
     * Retrieves the chosen symbol from this Player
     * 
     * @return string
     */
    public function getSymbol()
    {
        return $this->_symbol;
    }
}
As you can see, we only care about the symbol of the player. Apparently this is more than enough for now. But when we check our main TictactoeTest we see no specific tests for our Player class. About time to do something about it.
<?php
class PlayerTest extends PHPUnit_Framework_TestCase
{
    protected $_player;
    protected function setUp()
    {
        $this->_player = new Player();
        parent::setUp();
    }
    protected function tearDown()
    {
        $this->_player = null;
        parent::tearDown();
    }
    public function testPlayerHasNoSymbolAtStartup()
    {
        $this->assertNull($this->_player->getSymbol());
    }
    public function goodSymbolProvider()
    {
        return array (
            array ('O'),
            array ('X'),
        );
    }
    /**
     * @dataProvider goodSymbolProvider
     */
    public function testPlayerCanSetASymbol($symbol)
    {
        $this->_player->setSymbol($symbol);
        $this->assertEquals($symbol, $this->_player->getSymbol());
    }
}
In our code and in our tests we only set a symbol, but don't check if the symbol is valid. The rules of this game do specify that only a X or a O are allowed. So we need to add additional logic to our tests and code. Let's work on our test first.
    public function badSymbolProvider()
    {
        return array (
            array ('a'),
            array (123),
            array ('Hello World!'),
            array (0x123),
        );
    }
    /**
     * @dataProvider badSymbolProvider
     * @expectedException InvalidArgumentException
     */
    public function testPlayerThrowsExceptionWithBadSymbol($symbol)
    {
        $this->_player->setSymbol($symbol);
    }
By using the 'expectedException' annotation we can verify that a specific exception is thrown. For most exceptions you can often use one of the SPL exceptions. We choose 'InvalidArgumentException' as the symbol is an invalid argument for this Player class. Of course our tests will fail as we haven't forseen this feature or behaviour.
InvalidArgumentException
We need to modify our code so it can be throw an InvalidArgumentException when wrong arguments (read symbols) are being provided. We want our tests to succeed, right?
    /**
     * Sets a symbol for this Player
     * 
     * @param string $symbol
     * @return Player
     * @throws InvalidArgumentException
     */
    public function setSymbol($symbol)
    {
        $validSymbols = array (self::PLAYER_O, self::PLAYER_X);
        if (!in_array($symbol, $validSymbols)) {
            throw new InvalidArgumentException(
                'Player can only choose between X or O');
        }
        $this->_symbol = (string) $symbol;
        return $this;
    }
As you can see, it's just a small but significant change that ensures proper symbols are being used in our game. And our tests now succeed.
InvalidArgumentException
So we can increase the quality of the game by making sure only allowed symbols are used.

We also have a collection class called Players that contain both players. We could have used an array, but as we don't know yet what kind of functionality we want for all players. In our code we just created this collection that implements the SeekableIterator and Countable interfaces. Our PlayersTest should be able to add a Player object, even two Player objects but certainly not more than 2 players. In test code this becomes a simple straightforward test.
<?php
class PlayersTest extends PHPUnit_Framework_TestCase
{
    protected $_players;
    
    protected function setUp()
    {
        $this->_players = new Players();
        parent::setUp();
    }
    protected function tearDown()
    {
        parent::tearDown();
        $this->_players = null;
    }
    public function testPlayersIsEmptyAtStartUp()
    {
        $this->assertEmpty($this->_players->getPlayers());
    }
    public function testPlayerCanAddPlayer()
    {
        $this->_players->addPlayer(new Player());
        $this->assertEquals(1, count($this->_players));
    }
    public function testPlayerCanTwoPlayerObjects()
    {
        $this->_players->addPlayer(new Player())
                       ->addPlayer(new Player());
        $this->assertEquals(2, count($this->_players));
    }
    /**
     * @expectedException OverflowException
     */
    public function testPlayerCannotAddMoreThenTwoPlayerObjects()
    {
        $this->_players->addPlayer(new Player())
                       ->addPlayer(new Player())
                       ->addPlayer(new Player());
    }
}
Since we implement 2 interfaces we know the functionality of them, and therefor we should not test for it as they will throw errors if not implemented correctly. If you still doubt yourself on working with these interfaces, create a separate set of tests that just verifies your collection class is behaving as expected. Our collection class Players will look like this.
<?php
/**
 * TicTacToe
 * 
 * A simple game that's played with two players, each taking a turn by marking
 * a field in a grid of 3 x 3 with either an X or an O (one symbol per player).
 * Winner is the one who has 3 identical symbols in a single horizontal,
 * vertical or diagonal row.
 * 
 * @package Tictactoe
 * @license "Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)"
 * @link http://creativecommons.org/licenses/by-sa/3.0/
 */
/**
 * Players
 * 
 * This Players class is a collection container for both players, implementing
 * both Countable and SeekableIterator interfaces from SPL
 * 
 * @package Tictactoe
 * @category Tictactoe
 * @see SeekableIterator
 * @see Countable
 * @link http://php.net/spl
 *
 */
class Players implements SeekableIterator, Countable
{
    const MAX_PLAYERS = 2;
    /**
     * Contains Player objects
     * 
     * @var array
     */
    protected $_players = array ();
    /**
     * Specifies the position in the stack
     * 
     * @var int
     */
    protected $_position;
    /**
     * Keeps track of the count
     * 
     * @var int
     */
    protected $_count;
    /**
     * Constructor for this Players collection, allows setting players with
     * optional provided array of Player objects
     * 
     * @param array $array
     */
    public function __construct(array $array = null)
    {
        if (null !== $array) {
            foreach ($array as $player) {
                $this->addPlayer($player);
            }
        }
    }
    /**
     * Adds a Player object to the collection
     * 
     * @param Player $player
     * @return Players
     * @throw Overflow
     */
    public function addPlayer(Player $player)
    {
        if (self::MAX_PLAYERS <= $this->count()) {
            throw new OverflowException('Cannot add more Player objects');
        }
        $this->_players[] = $player;
        return $this;
    }
    /**
     * Retrieves a list of Player objects currently in the collection
     * 
     * @return array
     */
    public function getPlayers()
    {
        return $this->_players;
    }
    /**
     * Sets the internal stack pointer back to 0
     * 
     * @see SeekableIterator::rewind()
     */
    public function rewind()
    {
        $this->_position = 0;
    }
    /**
     * Sets the internal stack pointer to the next position
     * 
     * @see SeekableIterator::next()
     */
    public function next()
    {
        $this->_position++;
    }
    /**
     * Checks if the current position in the stack is still valid
     * 
     * @see SeekableIterator::valid()
     * @return boolean
     */
    public function valid()
    {
        return isset ($this->_player[$this->_position]);
    }
    /**
     * Retrieves the current object from the stack
     * 
     * @see SeekableIterator::current()
     * @return Player
     */
    public function current()
    {
        return $this->_players[$this->_position];
    }
    /**
     * Retrieves the current position from the stack
     * 
     * @see SeekableIterator::key()
     * @return int
     */
    public function key()
    {
        return $this->_position;
    }
    /**
     * Moves the position to the given stack postion
     * 
     * @see SeekableIterator::seek()
     * @throws OutOfBoundsException
     */
    public function seek($position)
    {
        if (!isset ($this->_players[$position])) {
            throw new OutOfBoundsException('No more items in this stack');
        }
        $this->_position = $position;
        return $this;
    }
    /**
     * Keeps record of the count in this collection
     * 
     * @see Countable::count()
     */
    public function count()
    {
        return count($this->_players);
    }
}
Ok, we now have our Grid and our Player objects tested. Next up is testing the game itself. Are all our tests taking care of all rules defined by the game? What are the edge cases? What is not acceptable? All this and more in the next eppisode of 'Quality Assurance on PHP projects'.


Comments

Popular posts from this blog

PHP 7 and Apache on macOS Sierra

I posted several talks about compiling PHP from source, but everyone was trying to convince me that a package manager like Homebrew was a more convenient way to install. The purpose of Homebrew is simple: a package manager for macOS that will allow you to set up and install common packages easily and allows you to update frequently using simple commands. I used a clean installation of macOS Sierra to ensure all steps could be recorded and tested. In most cases you already have done work on your Mac, so chances are you can skip a few steps in this tutorial. APACHE AND PHP WITH HOMEBREW I’ve made this according to the installation instructions given on GetGrav. The installation procedures These installation procedures will set up your macOS Sierra with PHP 7.1 and Apache 2.4. Install Xcode command line tools (if not done yet)xcode-select --install Install Homebrew/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" Set up for in…

Sessions in PHP 7.1 and Redis

In case you have missed it, PHP 7.1.0 has been released recently. Now you can’t wait to upgrade your servers to the latest and greatest PHP version ever. But hold that thought a second… With PHP 7 lots of things have changed underneath the hood. But these changed features can also put unexpected challenges on your path. Our challenge One of these challenges that we faced was getting PHP 7.1 to play nice storing sessions in our Redis storage. In order to store sessions in Redis, we needed to install the Redis PHP extension that not only provides PHP functions for Redis, but also installs the PHP session handler for Redis. Because we upgraded our servers to PHP 7.1, we were looking to use the latest provided version for this Redis extension: redis-3.1.0. Once installed, we bumped against a nasty problem. Warning: session_start(): Failed to read session data: redis (path: tcp://127.0.0.1:6379) Searching the internet for this error, we didn’t got many hits that could point us into a dire…

Speeding up database calls with PDO and iterators

When you review lots of code, you often wonder why things were written the way they were. Especially when making expensive calls to a database, I still see things that could and should be improved.
No framework development When working with a framework, mostly these database calls are optimized for the developer and abstract the complex logic to improve and optimize the retrieval and usage of data. But then developers need to build something without a framework and end up using the basics of PHP in a sub-optimal way.

$pdo = new \PDO( $config['db']['dsn'], $config['db']['username'], $config['db']['password'] ); $sql = 'SELECT * FROM `gen_contact` ORDER BY `contact_modified` DESC'; $stmt = $pdo->prepare($sql); $stmt->execute(); $data = $stmt->fetchAll(\PDO::FETCH_OBJ); echo 'Getting the contacts that changed the last 3 months' . PHP_EOL; foreach ($data as $row) { $dt = new \DateTime('2015-04-…