Matthew Turland
Linked from either of these:
Slides or video from php[world] 2014 tutorial
Install using Composer:
{
"require-dev": {
"phake/phake": "^2"
}
}
Or clone the git repository:
github.com/mlively/Phake
Seriously, just use Composer.
In computer programming, unit testing is a software testing method... The goal of unit testing is to isolate each part of the program and show that the individual parts are correct.Wikipedia
PHPUnit is a programmer-oriented testing framework for PHP. It is an instance of the xUnit architecture for unit testing frameworks.PHPUnit project web site
Integration testing... is the phase in software testing in which individual software modules are combined and tested as a group. It occurs after unit testing...Wikipedia
public function testGetUsers() {
$model = new UserModel(new \PDO('...'));
$users = $model->getUsers();
// ...
}
use GuzzleHttp\ClientInterface;
class FooRestClient {
protected $client;
public function __construct(ClientInterface $client) {
$this->client = $client;
/* ... */
}
/* ... */
}
How do we test this class in isolation from the
ClientInterface
instance injected into its
constructor?
In automated unit testing, it may be necessary to use objects... that look and behave like their release-intended counterparts, but are actually simplified versions... A test double is a generic... term used for these objects...Wikipedia
In object-oriented programming, mock objects are simulated objects that mimic the behavior of real objects in controlled ways... to test the behavior of some other object.Wikipedia
For mock objects to pass typehint checks:
Using the previous class under test example:
class FooRestClientTest extends PHPUnit_Framework_TestCase {
public function testConstructor() {
$http = Phake::mock(ClientInterface::class);
echo get_class($http), PHP_EOL;
$client = new FooRestClient($http);
}
}
ClientInterface_PHAKE5556f19936c6c
The practice of replacing an object with a test double that... returns configured return values is referred to as stubbing.PHPUnit manual
use GuzzleHttp\ClientInterface;
class FooRestClient {
/* ... */
public function getThing($id) {
// The two methods called below need to be stubbed
$response = $this->client->get(
'https://api.foo.com/things/' . $id
);
return $response->json();
}
}
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Message\ResponseInterface;
$data = ['id' => 1, 'name' => 'Thing One'];
$response = Phake::mock(ResponseInterface::class);
Phake::when($response)->json()->thenReturn($data);
$http = Phake::mock(ClientInterface::class);
Phake::when($http)
->get('https://api.foo.com/things/1')
->thenReturn($response);
$client = new FooRestClient($http);
$thing = $client->getThing('1');
$this->assertSame($thing, $data);
Phake refers to these as answers.
By default, mock object methods return null
.
thenReturn($value)
thenReturnCallback($callable)
thenThrow($exception)
private
Methodsprivate
methods can't be overridden in subclasses and thus can't be stubbedpublic
methods on mockspublic
methods by testing the public
methods that call themprivate
Methods
class MyReallyTerribleOldClass {
private function cleanRowContent($dbRow) {
return 42;
}
}
$dbRow = ['foo' => 'bar'];
$oldClass = Phake::partialMock('MyReallyTerribleOldClass');
$data = Phake::makeVisible($oldClass)->cleanRowContent($dbRow);
private
Methods
class PrivateExample {
private function privateMethod() {
return 42;
}
}
// ...
$method = new \ReflectionMethod('PrivateExample', 'privateMethod');
$method->setAccessible(true);
$this->assertEquals(42, $method->invoke(new PrivateExample));
static
Methodsstatic
methods can't be stubbed
unless they're invoked using late static binding
static
Method Calls
class Foo {
public $logger = 'Logger';
public function doSomething() {
$logger = $this->logger;
$logger::logData($data);
}
}
class FooTest {
public function testDoSomething() {
$foo = new Foo;
$foo->logger = Phake::mock('Logger');
$foo->doSomething();
Phake::verify($foo->logger)
->logData(Phake::anyParameters());
}
}
static
Method Calls
class ClassWithStaticMethod {
public static function staticMethod() {
return 42;
}
}
class DependentClass {
/* Wrap the static method call in an instance method ... */
protected function callStaticMethod() {
return ClassWithStaticMethod::staticMethod();
}
}
$mock = Phake::mock('DependentClass');
/* ... then stub that instance method */
Phake::when($mock)->callStaticMethod()->thenReturn(37);
final
Methodsfinal
methods can't be stubbed
because subclasses can't override them
class FinalExample {
public final function finalMethod() {
return 42;
}
}
$mock = Phake::mock('FinalExample');
Phake::when($mock)->finalMethod()->thenReturn(37);
var_dump($mock->finalMethod());
int(42)
final
classes can't be mocked or stubbed at all because they can't be subclassed.
final
Method Calls
class ClassWithFinalMethod {
public final function finalMethod() {
return 42;
}
}
class DependentClass {
private $instance;
public function __construct(ClassWithFinalMethod $instance) {
$this->instance = $instance;
}
/* Wrap the final method call in an instance method ... */
protected function callStaticMethod() {
return $this->instance->finalMethod();
}
}
$mock = Phake::mock('ClassWithFinalMethod');
/* ... then stub that instance method */
Phake::when($mock)->callFinalMethod()->thenReturn(37);
All magic methods can be stubbed normally
except for __call()
class ClassWithCall {
public function __call($method, $args) {
return 42;
}
}
$mock = Phake::mock('ClassWithCall');
/* Either stub the call that would trigger __call() ... */
Phake::when($mock)->nonExistentMethod()->thenReturn(37);
/* ... or stub __call() itself */
Phake::whenCallMethodWith('nonExistentMethod', [])
->isCalledOn($mock)->thenReturn(37);
class Adder {
public function add($x, $y) {
return $x + $y;
}
}
$mock = Phake::mock('Adder');
Phake::when($mock)->add(0, 2)->thenReturn(2);
Phake::when($mock)->add(2, -2)->thenReturn(0);
Phake::when($mock)->add(0, -2)->thenReturn(-2);
class FileObject {
public function readBytes() { /* ... */ }
}
$mock = Phake::mock('FileObject');
Phake::when($mock)->readBytes()
->thenReturn('foo') // first call
->thenReturn('bar') // second call
->thenReturn('baz'); // third and subsequent calls
class OverwriteExample {
public function getValue() { /* ... */ }
}
$mock = Phake::mock('OverwriteExample');
/* Define the stub */
Phake::when($mock)->getValue()->thenReturn(42);
/* Then re-define to overwrite it */
Phake::when($mock)->getValue()->thenReturn(37);
var_dump($mock->getValue());
int(37)
Mock objects in Phake can almost be viewed as a tape recorder... Phake::verify()
will look at that recording and allow you to assert whether or not a certain call was made.
class EventEmitter {
public function emit($event, $args) { /* ... */ }
}
class Plugin {
public function __construct(EventEmitter $emitter) {
$this->emitter = $emitter;
}
public function doThing() {
/* This call must be verified */
$this->emitter->emit('thing.happened', ['stuff!']);
}
}
$emitter = Phake::mock('EventEmitter');
$plugin = new Plugin($emitter);
$plugin->doThing();
Phake::verify($emitter)->emit('thing.happened', ['stuff!']);
class MultipleCallClass {
public function calledMethod($arg) { /* ... */ }
}
$mock = Phake::mock('MultipleCallClass');
$mock->calledMethod('foo');
$mock->calledMethod('bar');
Phake::verify($mock)->calledMethod('bar');
Phake::verify($mock)->calledMethod('foo');
class OrderClass {
public function doThing() { /* ... */ }
public function doLastThing() { /* ... */ }
public function doOtherThing() { /* ... */ }
}
$mock = Phake::mock('OrderClass');
$mock->doThing();
$mock->doOtherThing();
$mock->doLastThing();
Phake::inOrder(
Phake::verify($mock)->doThing(),
Phake::verify($mock)->doOtherThing(),
Phake::verify($mock)->doLastThing()
);
$mock->calledMethod('foo');
$mock->calledMethod('foo');
Phake::verify($mock, Phake::times(2))->calledMethod('foo');
These also work:
Phake::atLeast($n)
Phake::atMost($n)
Phake::never()
class ThingDoer {
public function doThing() { /* ... */
}
class StuffDoer {
public function __construct(ThingDoer $thing) {
$this->thing = $thing;
}
public function doStuff($doThing) {
if ($doThing) { $this->thing->doThing(); }
}
}
$thing = Phake::mock('ThingDoer');
$stuff = new StuffDoer($thing);
$stuff->doStuff(false);
Phake::verifyNoInteraction($thing);
$thing = Phake::mock('ThingDoer');
$stuff = new StuffDoer($thing);
$stuff->doStuff(true);
Phake::verifyNoFurtherInteraction($thing);
$stuff->doStuff(false);
$filter = new MyFilter();
$list = Phake::Mock('MyList');
$filter->addEvenToList([1, 2, 3, 4, 5], $list);
Phake::verify($list)->push(2);
Phake::verify($list)->push(4);
Phake::verifyNoOtherInteractions($list);
Same story, second verse as stubbing magic methods
class ClassWithCall {
public function __call($method, $args) {
return 42;
}
}
$mock = Phake::mock('ClassWithCall');
/* Either stub the call that would trigger __call() ... */
Phake::verify($mock)->nonExistentMethod();
/* ... or verify the call to __call() itself */
Phake::verifyCallMethodWith('nonExistentMethod', [])
->isCalledOn($mock);
Phake supports using PHPUnit constraints to make assertions about stubbed and verified calls.
class Shuffler {
public function shuffle($times) { /* ... */ }
}
class ShufflerTest extends PHPUnit_Framework_TestCase {
public function testShuffle() {
$shuffler = Phake::mock('Shuffler');
$shuffler->shuffle(11);
Phake::verify($shuffler)->shuffle($this->greaterThan(10));
/* ^ the constraint */
}
}
Custom matchers are also supported.
anything()
matcher
Phake::when($mock)->methodName->thenReturn('bar');
/* ^
note the lack of parentheses here */
Phake::when($mock)
->methodName('foo', 'bar', Phake::ignoreRemaining())
->thenReturn('bar');
$this->bot->run();
$client->emit('irc.received', [
/* array */ $message,
/* object */ $write,
/* object */ $connection,
/* object */ $logger
]);
Phake::verify($client)->emit(
'irc.sending.all',
Phake::capture($allParams)
);
$this->assertSame($queue, $allParams[0]);
Phake::verify($client)->emit(
'irc.sending.all',
Phake::capture($allParams)->when($this->isType('array'))
/* ^
uses the same matchers verify() supports */
);
$http = $this->getMockBuilder(ClientInterface::class)
->disableOriginalConstructor()
->disableOriginalClone()
->getMock();
// or:
$response = $this->getMock(
ClientInterface::class,
[], // stub all methods
[], // no constructor arguments
'', // default mock class name
false, // do not call original __construct()
false // do not call original __clone()
);
// Compare with:
$mock = Phake::mock(ClientInterface::class);
Stubbing and verification APIs are conflated.
$http->expects($this->once()) // verification
->method('get')
->with('https://api.foo.com/things/1')
->willReturn($response); // stubbing
// Compare with:
Phake::when($http)
->get('https://api.foo.com/things/1')
->thenReturn($response);
Phake::verify($http)
->get('https://api.foo.com/things/1');
$mock->method('doThing')
->will($this->returnValueMap([
['arg1-1', 'arg1-2', 'ret1'],
['arg2-1', 'arg2-2', 'ret2'],
]));
// or:
$mock->method('doThing')
->will($this->returnCallback(
function($arg1, $arg2) {
// ...
}
);
// Compare with:
Phake::when($mock)
->doThing('arg1-1', 'arg1-2')
->thenReturn('ret1');
Phake::when($mock)
->doThing('arg2-1', 'arg2-2')
->thenReturn('ret2');
$mock->expects($this->at(0))
->method('doThing')
->with('arg1', 'arg2')
->willReturn('foo');
$mock->expects($this->at(1))
// ...
;
// Compare with:
Phake::inOrder(
Phake::verify($mock)
->doThing('arg1', 'arg2')
->thenReturn('foo'),
Phake::verify($mock)
// ...
);
Here are some alternatives:
Please rate my talk!
Also, check out the joind.in mobile apps!