Testing abstract classes (Intermediate)
When we were discussing mock objects the concept of partial mocks was introduced. One common use of partial mocks is to test abstract classes. Abstract classes can't be tested directly as by definition they cannot be instantiated. You can always create an extension of the abstract class just for testing. However, PHPUnit provides functionality to very easily mock abstract classes so that only the abstract methods get mocked. All other functions will execute normally.
How to do it...
In src/Player.php
is a Player
class shown as follows:
<?php abstract class Player { // ... public function requestCard() { $cardNumber = $this->chooseCardNumber(); if (!$this->hasCard($cardNumber)) { throw new RuntimeException('Invalid card chosen by player'); } return $cardNumber; } abstract protected function chooseCardNumber(); // ... }
The corresponding test can be placed in test/PlayerTest.php
to test the abstract nature of the class.
<?php class PlayerTest extends PHPUnit_Framework_TestCase { private $player; private $hand; public function setUp() { $this->hand = new CardCollection(); $this->hand->addCard(new Card('A', 'Spades')); $this->player = $this->getMockForAbstractClass('Player', array('John Smith', $this->hand)); } public function testRequestCardCallsChooseCardNumber() { $this->player->expects($this->once()) ->method('chooseCardNumber') ->will($this->returnValue('A')); $this->assertEquals('A', $this->player->requestCard()); } }
How it works...
The PHPUnit method getMockForAbstractClass()
can be used to generate a partial mock where only the abstract methods of a given class are overridden. The argument list for getMockForAbstractClass()
is similar to the argument list for getMock()
. The big difference is that the list of methods to mock is moved from being the second parameter to being the last parameter. By default getMockForAbstractClass()
will mock only the abstract methods of the class. If you find yourself needing to override this functionality then you should just use getMock()
instead.
In this example, the Player
class is being mocked with a player name and a CardCollection
object is being passed to the Player
instance's constructor. The testRequestCardCallsChooseCardNumber()
method is assuring that the Player::chooseCardNumber()
method is called as a part of Player::requestCard()
and is then ensuring that the value returned by chooseCardNumber()
is subsequently returned by requestCard()
.
You could use getMock()
for this instead. The setUp()
method could be rewritten to use getMock()
to set up the partial mock.
public function setUp() { $this->hand = new CardCollection(); $this->hand->addCard(new Card('A', 'Spades')); $this->player = $this->getMock('Player', array('chooseCardNumber'), array('John Smith', $this->hand)); }
The advantage of using getMockForAbstractClass()
is that you do not have to add to the mocked method list (the second parameter of getMock()
) every time you add a new abstract method to the class. It also keeps the test significantly more concise.
Abstract classes in Phake
Phake also provides a function that assists in testing abstract classes. Phake::partialMock()
works in a similar fashion to the PHPUnit counterpart.
<?php class PhakePlayerTest extends PHPUnit_Framework_TestCase { private $player; private $hand; public function setUp() { $this->hand = new CardCollection(); $this->hand->addCard(new Card('A', 'Spades')); $this->player = Phake::partialMock('Player', 'John Smith', $this->hand); } public function testRequestCardCallsChooseCardNumber() { Phake::when($this->player)->chooseCardNumber()->thenReturn('A'); $this->assertEquals('A', $this->player->requestCard()); Phake::verify($this->player)->chooseCardNumber(); } }
The Phake::partialMock()
method accepts the class name as the first parameter. The remaining parameters will be used in the constructor of the mock object. This method works in mostly the same way as getMockForAbstractClass()
. It creates a mock that will call the original method for any non-abstract method.