Prophecy
Highly opinionated mocking framework for PHP 5.3+
Install / Use
/learn @phpspec/ProphecyREADME
Prophecy
Prophecy is a highly opinionated yet very powerful and flexible PHP object mocking framework. Though initially it was created to fulfil phpspec2 needs, it is flexible enough to be used inside any testing framework out there with minimal effort.
A simple example
<?php
class UserTest extends PHPUnit\Framework\TestCase
{
private $prophet;
public function testPasswordHashing()
{
$hasher = $this->prophet->prophesize('App\Security\Hasher');
$user = new App\Entity\User($hasher->reveal());
$hasher->generateHash($user, 'qwerty')->willReturn('hashed_pass');
$user->setPassword('qwerty');
$this->assertEquals('hashed_pass', $user->getPassword());
}
protected function setUp()
{
$this->prophet = new \Prophecy\Prophet;
}
protected function tearDown()
{
$this->prophet->checkPredictions();
}
}
Installation
Prerequisites
Prophecy requires PHP 7.2.0 or greater.
Setup through composer
First, add Prophecy to the list of dependencies inside your composer.json:
{
"require-dev": {
"phpspec/prophecy": "~1.0"
}
}
Then simply install it with composer:
$> composer install --prefer-dist
You can read more about Composer on its official webpage.
How to use it
First of all, in Prophecy every word has a logical meaning, even the name of the library itself (Prophecy). When you start feeling that, you'll become very fluid with this tool.
For example, Prophecy has been named that way because it concentrates on describing the future behavior of objects with very limited knowledge about them. But as with any other prophecy, those object prophecies can't create themselves - there should be a Prophet:
$prophet = new Prophecy\Prophet;
The Prophet creates prophecies by prophesizing them:
$prophecy = $prophet->prophesize();
The result of the prophesize() method call is a new object of class ObjectProphecy. Yes,
that's your specific object prophecy, which describes how your object would behave
in the near future. But first, you need to specify which object you're talking about,
right?
$prophecy->willExtend('stdClass');
$prophecy->willImplement('SessionHandlerInterface');
There are 2 interesting calls - willExtend and willImplement. The first one tells
object prophecy that our object should extend a specific class. The second one says that
it should implement some interface. Obviously, objects in PHP can implement multiple
interfaces, but extend only one parent class.
Dummies
Ok, now we have our object prophecy. What can we do with it? First of all, we can get our object dummy by revealing its prophecy:
$dummy = $prophecy->reveal();
The $dummy variable now holds a special dummy object. Dummy objects are objects that extend
and/or implement preset classes/interfaces by overriding all their public methods. The key
point about dummies is that they do not hold any logic - they just do nothing. Any method
of the dummy will always return null and the dummy will never throw any exceptions.
Dummy is your friend if you don't care about the actual behavior of this double and just need
a token object to satisfy a method typehint.
You need to understand one thing - a dummy is not a prophecy. Your object prophecy is still
assigned to $prophecy variable and in order to manipulate with your expectations, you
should work with it. $dummy is a dummy - a simple php object that tries to fulfil your
prophecy.
Stubs
Ok, now we know how to create basic prophecies and reveal dummies from them. That's awesome if we don't care about our doubles (objects that reflect originals) interactions. If we do, we need to use stubs or mocks.
A stub is an object double, which doesn't have any expectations about the object behavior, but when put in specific environment, behaves in specific way. Ok, I know, it's cryptic, but bear with me for a minute. Simply put, a stub is a dummy, which depending on the called method signature does different things (has logic). To create stubs in Prophecy:
$prophecy->read('123')->willReturn('value');
Oh wow. We've just made an arbitrary call on the object prophecy? Yes, we did. And this
call returned us a new object instance of class MethodProphecy. Yep, that's a specific
method with arguments prophecy. Method prophecies give you the ability to create method
promises or predictions. We'll talk about method predictions later in the Mocks section.
Promises
Promises are logical blocks, that represent your fictional methods in prophecy terms
and they are handled by the MethodProphecy::will(PromiseInterface $promise) method.
As a matter of fact, the call that we made earlier (willReturn('value')) is a simple
shortcut to:
$prophecy->read('123')->will(new Prophecy\Promise\ReturnPromise(array('value')));
This promise will cause any call to our double's read() method with exactly one
argument - '123' to always return 'value'. But that's only for this
promise, there's plenty others you can use:
ReturnPromiseor->willReturn(1)- returns a value from a method callReturnArgumentPromiseor->willReturnArgument($index)- returns the nth method argument from callThrowPromiseor->willThrow($exception)- causes the method to throw specific exceptionCallbackPromiseor->will($callback)- gives you a quick way to define your own custom logic
Keep in mind, that you can always add even more promises by implementing
Prophecy\Promise\PromiseInterface.
Method prophecies idempotency
Prophecy enforces same method prophecies and, as a consequence, same promises and predictions for the same method calls with the same arguments. This means:
$methodProphecy1 = $prophecy->read('123');
$methodProphecy2 = $prophecy->read('123');
$methodProphecy3 = $prophecy->read('321');
$methodProphecy1 === $methodProphecy2;
$methodProphecy1 !== $methodProphecy3;
That's interesting, right? Now you might ask me how would you define more complex behaviors where some method call changes behavior of others. In PHPUnit or Mockery you do that by predicting how many times your method will be called. In Prophecy, you'll use promises for that:
$user->getName()->willReturn(null);
// For PHP 5.4
$user->setName('everzet')->will(function () {
$this->getName()->willReturn('everzet');
});
// For PHP 5.3
$user->setName('everzet')->will(function ($args, $user) {
$user->getName()->willReturn('everzet');
});
// Or
$user->setName('everzet')->will(function ($args) use ($user) {
$user->getName()->willReturn('everzet');
});
And now it doesn't matter how many times or in which order your methods are called. What matters is their behaviors and how well you faked it.
Note: If the method is called several times, you can use the following syntax to return different values for each call:
$prophecy->read('123')->willReturn(1, 2, 3);
This feature is actually not recommended for most cases. Relying on the order of calls for the same arguments tends to make test fragile, as adding one more call can break everything.
Arguments wildcarding
The previous example is awesome (at least I hope it is for you), but that's not
optimal enough. We hardcoded 'everzet' in our expectation. Isn't there a better
way? In fact there is, but it involves understanding what this 'everzet'
actually is.
You see, even if method arguments used during method prophecy creation look
like simple method arguments, in reality they are not. They are argument token
wildcards. As a matter of fact, ->setName('everzet') looks like a simple call just
because Prophecy automatically transforms it under the hood into:
$user->setName(new Prophecy\Argument\Token\ExactValueToken('everzet'));
Those argument tokens are simple PHP classes, that implement
Prophecy\Argument\Token\TokenInterface and tell Prophecy how to compare real arguments
with your expectations. And yes, those classnames are damn big. That's why there's a
shortcut class Prophecy\Argument, which you can use to create tokens like that:
use Prophecy\Argument;
$user->setName(Argument::exact('everzet'));
ExactValueToken is not very useful in our case as it forced us to hardcode the username.
That's why Prophecy comes bundled with a bunch of other tokens:
IdenticalValueTokenorArgument::is($value)- checks that the argument is identical to a specific valueExactValueTokenorArgument::exact($value)- checks that the argument matches a specific valueTypeTokenorArgument::type($typeOrClass)- checks that the argument matches a specific type or classnameObjectStateTokenorArgument::which($method, $value)- checks that the argument method returns a specific valueCallbackTokenorArgument::that(callback)- checks that the argument matches a custom callbackAnyValueTokenorArgument::any()- matches any argumentAnyValuesTokenorArgument::cetera()- matches any arguments to the rest of the signatureStringContainsTokenorArgument::containingString($value)- checks that the argument contains a specific string valueInArrayTokenorArgument::in($array)- checks if value is in arrayNotInArrayTokenorArgument::notIn($array)- checks if value is not in array
And you can add even more by implementing TokenInterface with your own custom classes.
So, let's refactor our initial {set,get}Name() logic with argument tokens:
use Prophecy\Argument;
$user->getName()->willReturn(null);
//
