Agile Zone is brought to you in partnership with:

I am a programmer and architect (the kind that writes code) with a focus on testing and open source; I maintain the PHPUnit_Selenium project. I believe programming is one of the hardest and most beautiful jobs in the world. Giorgio is a DZone MVB and is not an employee of DZone and has posted 637 posts at DZone. You can read more from them at their website. View Full User Profile

Practical PHP Testing Patterns: Mock Object

03.14.2011
| 8143 views |
  • submit to reddit

The Test Doubles we have seen until now are rather passive: they provide predefined results or record calls, but make no decision on their own. A Mock Object instead, is more than that: it verifies if it's used correctly, by making implicit assertions on what you pass it.

Method calls are the communication medium in object-oriented programming: a Mock Object is a Test Double which performs verifications on the calls made on it and their parameters, verifying thus the indirect output of the System Under Test (its side-effects instead of its returned values).

Mocks implement Behavior Verification at its best; Stubs and Mocks are also the most commonly used Test Doubles. Remember that the difference is only that the Mock actively checks what you pass him and how you call it.

Checking parameters and calls is the Mock's job, but it may need to return something to allow the SUT not to fail or explode. Consider the contract and simplify it as most as possible to limit the Mock's complexity: this is another selling point for Test Doubles.

Implementation

Mocks are greatly supported by PHPUnit, which even calls all Test Doubles mocks.

However, PHPUnit but mixes up definition of expectations with definitions of canned results, so it's not really clear when you're just defining a Stub and when you're creating a Mock. Basically, when you're using with(), or will($this->returnCallback()) with a callback that makes assertions or throw exceptions, you have a Mock.

The steps required for creating a mock are:

  • create a mock by subclassing or coding an alternate implementation of an interface, as for all test doubles. Instance an object of your new class (PHPUnit does both steps for you out-of-the-box).
  • Define expectations in the arrange phase of the test.
  • Install the Test Double into the SUT.
  • Exercise the SUT by calling its methods.

Some verifications will be made immediately by the Mock(on parameters of a call), some other at the end of the test (like a method that had to be called 2 times but wasn't).

Note that PHPUnit clones parameter passed to Mocks in order to perform a strong verification at the end of the test. This will cause problems for example with isIdenticalTo() as the object used for verification is a clone and it's never identical (read: ===) to the one in the specification; use isInstanceOf() instead for a less strict expectation.

Variations

An hand-rolled Mock can be constructed by passing $this (the Testcase Object) to its constructor, or by simply throwing exceptions where the calls are incorrectly made (if the SUT allows exceptions to bubble up.)

Hand-rolled mocks may need additional step for verification, such as an assertion over the number of cals.

Generated mocks are one of the selling points of PHPUnit: the testing framework generates subclasses and instances an object for you; then you can configure the Mock expectations via a simple Api. In this case, the verification is always automatic, since PHUnit keeps a list of all instanced Mock Objects.

Examples

The code sample shows you two test which make use of Mocks. In the first, the Mock checks the number of calls it receives. In the second the Mock checks the passed parameters, and the number of calls which has to be 1.

Note that PHPUnit does not support different parameters expectations for different calls, but you can implement them with $this->returnCallback() and a custom closure.

<?php
class MockObjectTest extends PHPUnit_Framework_TestCase
{
/**
* In this test the UsersView Mock Object checks
* that it is called the right number of times
*/
public function testPrintsOnlyActiveUsers()
{
$users = array(
new User('george', true),
new User('john', false),
new User('mark', false),
new User('joan', true),
new User('steve', false)
);

$view = $this->getMock('UsersView');
$view->expects($this->exactly(2))
->method('add');

$sut = new UsersController($users);
$sut->renderOn($view);
}

public function testUsersSelectUsersWhoseNameStartsWithAGivenPrefix()
{
$users = array(
new User('george', true),
$john = new User('john', true),
new User('mark', true),
new User('steve', true)
);

$view = $this->getMock('UsersView');
$view->expects($this->once())
->method('add')
->with($john);

$sut = new UsersController($users, 'j');
$sut->renderOn($view);
}

}

/**
* The interface the Mock Objects implement. The simpler this interface,
* the cleaner your code.
*/
interface UsersView
{
public function add(User $user);
}

/**
* The System Under Test. It should render on a View, which is substituted by
* a Test Double.
*/
class UsersController
{
private $users;
private $prefixFilter;

public function __construct(array $users, $prefixFilter = '')
{
$this->users = $users;
$this->prefixFilter = $prefixFilter;
}

public function renderOn(UsersView $view)
{
foreach ($this->users as $user)
{
if ($user->isActive() && $user->startsWith($this->prefixFilter)) {
$view->add($user);
}
}
}
}

/**
* In these tests the instances of User will actually be Dummy, which
* means just objects which are passed around without any method call
* is performed on them.
* This implementation is thus really brief.
*/
class User
{
private $name;
private $active;

public function __construct($name, $active)
{
$this->name = $name;
$this->active = $active;
}

public function isActive()
{
return $this->active;
}

public function startsWith($prefix)
{
if ($prefix == '') {
return true;
}
if (strstr($this->name, $prefix) == $this->name) {
return true;
}
return false;
}
}
Published at DZone with permission of Giorgio Sironi, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)