Did you know that “under the hood” PHPSpec use Prophecy as library to create doubles? If you did not, now you do!
If you read Prophecy documentation you may end up with some questions, so this guide is meant to be a quick tutorial to let you start working with the library: nothing more, nothing less.
With this article we want to try to pack all useful information that are not explicitly written in the guide, providing a quick reference about doubles mechanisms.
First of all, let define a generic double.
A double is a “fake” instance of a class that, depending on its usage, can vary from trivial placeholder to complex object on which you can make expectations about method calls or “teach” what to return when methods are invoked.
Prophecy provides four different types of doubles:
- Dummy
- Stub
- Mock
- Spy
It’s worth to note that these terms are common in BDD/TDD and their libraries but can take different meanings; as we work with PHPSpec, our goal is to clarify them through examples.
Dummy
This is the most trivial usage of a double. You can choose to work with dummies when you don’t care about double behavior but you need to pass it anyway. A practical and common example is a function parameter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Foo { private $fooCollaborator; public function __construct(FooCollaborator $fooCollaborator) { $this->fooCollaborator = $fooCollaborator } public function useDummy(Dummy $dummy) { // some *not so important* logic here $this->fooCollaborator($dummy); // some other *not so important* logic here return true; } } |
Dummy
it’s not a special class, but class name we decided to use in order to describe what we use
1 2 3 4 |
class Dummy { // Don't care about its content } |
Let’s ignore fooCollaborator
for the moment. Spec file will look like this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class FooSpec { function it_is_initializable() { $this->shouldHaveType(Foo::class); } function let(FooCollaborator $fc) { $this->beConstructedWith($fc); } function it_use_dummy(Dummy $dummy) { $this->useDummy($dummy)->shouldReturn(true); } } |
What you can notice is that we use $dummy
directly: no expectations, no “canned method responses”, nothing: it is a sort of placeholder that you need to use to fulfill code requirements. When testing Foo
behavior it’s possible that you are not interested in $dummy
object.
Dummy object can be also created directly with Prophecy
1 2 |
$prophet = new Prophet(); $dummy = $prophet->prophesize(Dummy::class); |
Internally every method called on the dummy will return null
.
Again, using $dummy
directly without enhancing it with expectations or “canned method response” makes it a dummy.
From last sentence you may guess, at this point, that’s no way to specify what type of double you need; in fact, we just pass class name in one case – Dummy
is just a simple class, no interface implementation, no extension of “special library object” – or use prophet in latter one. This guess is absolutely right! Don’t worry if it seems a little bit fuzzy: next examples and final recap will point out the differences and the way to use one or other type of double.
Stub
Stub are used when you want to provide a canned response to method calls. They are very useful when you are describing a class that relies, for example, on API response: you don’t want tests fail because API fail or because there is no internet connection, for instance. API define a message format and you can “teach” stub method to return that kind of message. As PHPSpec is used to describe a behavior of a single class, you should worry only about that class, stubbing (or mocking as you will see) every other collaborator. What you need absolutely to keep in mind is that if collaborator response changes, you should update your test (and probably your code) as well, otherwise you’ll not be aware of errors introduced. I would like to highlight that this is only a quick example to illustrate what is a canned response and when it’s useful: below Mock section, you will find some links that I recommend to read about this particular topic and why not to stub or mock something that you don’t own like an API.
To make test double a stub just use the willReturn
method upon it
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class RandomClassSpec { public function it_test_something(Stub $stub) { // ... $stub->aMethod()->willReturn(true); // ... } public function it_test_something_else() { $prophet = new Prophet(); // ... $stub = $prophet->prophesize(Stub::class); $stub->aMethod()->willReturn(true); // ... } } |
$stub
is now a stub test double (again, if you pass it through PHPSpec argument injection or creating it with Prophet
makes no difference)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class Foo { public function foo() { return ['a' => 'a', 'b' => 'b']; } } class Bar { private $foo; public function __construct(Foo $foo) { $this->foo = $foo; } public function bar() { $result = $this->foo->foo(); return $result['b']; } } |
1 2 3 4 5 6 7 8 |
class BarSpec { public function it_run_bar(Foo $fooStub) { $fooStub->foo()->willReturn(['a' => 'b', 'b' => 'a']); // reverse actual foo function output $this->bar()->shouldReturn('a'); } } |
It’s worth to notice that’s mandatory, when test double is a Stub, to have a one-to-one correspondence between method calls you have in codebase and what you “fake” in spec. PHPSpec have three kind of status for an example: success, fail, broken. If you break previous rule, you’ll end up with a broken example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
class Foo { public function foo() { return ['a' => 'a', 'b' => 'b']; } public function makeExampleBroken() { // ... } } class Bar { private $foo; public function __construct(Foo $foo) { $this->foo = $foo; } public function bar() { $this->foo->makeExampleBroken(); // !!! $result = $this->foo->foo(); return $result['b']; } } |
Making no changes to BarSpec
will produce
it run bar
method call:
– makeExampleBroken()
on Double\Foo\P3 was not expected, expected calls were:
– foo()
Now we are in a broken state: as Prophecy is highly opinionated and PHPSpec is used to describe (not test!) object behavior and let emerge a good design, that’s seems to be absolutely legit: you are describing interaction with a collaborator so every call that is recorded but not declared in the spec, will produce this state. Is your test failed? No, it’s not, because expectations are not failed (yet). Broken status can be fixed easily (just add willReturn
to the stub) or not (what is returned and used alter behavior so you’ll end up with a failed example that needs some extra effort to be turned into a success).
Mock
Mocks are used when you want to make expectations on collaborators. An expectation is an assertion (conceptually): if it’s not satisfied, an exception will be raised and the example end with a fail status.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Bar { private $mock; public function __construct(Mock $mock) { $this->mock = $mock; } public function bar($arg) { $this->mock->doSomething($arg); return true; } } |
1 2 3 4 5 6 7 8 9 10 11 |
class BarSpec { public function it_does_something(Mock $mock) { $arg = true; $mock->doSomething($arg)->shouldBeCalled(); // satisfy the expectation $mock->doSomething(false)->shouldBeCalled(); // not satisfy $mock->doSomething('foo')->shouldNotBeCalled(); // satisfy the expectation $this->foo($arg)->shouldReturn(true); } } |
Again that’s no difference if we pass argument as an argument of spec example or if it is created with Prophet
as shown for others test dummies.
There are three interesting things about mocks that are worth to know
- If not all method calls in SUS are registered as expectations in spec example test will fail
- All method calls should be registered with correct arguments (or with arguments wildcards) otherwise example will fail
- Mocks can have “fake” methods return values like stubs. The natural consequence of this sentence is that if a double have at least an expectation, it becomes (it has the behavior of) a mock.
Mocks are a good way to isolate examples: when mock something, only thing that you should care is about methods calls made (or not) upon that object. Mock behavior is described through examples (read it as “tested” in TDD approach) in other spec files. This will “guarantee” that if mock change introducing some bugs or unexpected behavior, not all spec where this mock is used end up with a fail status. At the same time if you have another spec for this mock you will be aware of this and fix will be pretty quick.
There’s a golden rule about mocks and, often, is totally ignored
DON’T MOCK WHAT YOU DON’T OWN
I’m not gonna write about that because you can find online pretty good resources like
- https://github.com/mockito/mockito/wiki/How-to-write-good-tests#dont-mock-type-you-dont-own
- https://adamwathan.me/2017/01/02/dont-mock-what-you-dont-own/
- https://8thlight.com/blog/eric-smith/2011/10/27/thats-not-yours.html
If this explanation about mocks isn’t enough for you, you can read our old article “About testing entity state changes (in PHPSpec)”
Spy
In other mock framework/libraries (like mockito), spies are to be treated as partial mocks: if a method is stubbed, then stub result is returned and method will not be called, otherwise call is made and “normal behavior” is what you get.
With Prophecy, spies are mocks with a “special privilege”: you’re not forced to make expectations to every method call. In other words, you can make expectations/assertion only on a subset of method of the test double. Moreover, with prophecy, spies are not partial mocks: if a method is not stubbed, null
will be always returned.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Spy { public function first() { return 1; } public function second() { return 2; } } class Foo { public function useSpy(Spy $spy) { $spy->first(); return $spy->second(); } } |
1 2 3 4 5 6 7 |
function it_use_spy(Spy $spy) { // $this->useSpy($spy)->shouldReturn(2); //will fail as method is not stubbed $this->useSpy($spy); $spy->first()->shouldHaveBeenCalled(); // this is a spy; test will not fail even if `second` is not expected } |
Spies are useful if you need to be sure that a method is called (or not) onto test double, but if you don’t want to be forced to specify every method call made on that test double (like Mock).
Conclusions
That’s it. Now you have a complete overview of test doubles in PHPSpec / Prophecy. If you would like to try yourself, you can clone or fork this repository: I’ve commented out some sections that will let example fails. If you have any doubt, don’t hesitate to leave a comment here or to open an issue on that repository.