Why should you upgrade your test suite to PhpSpec & Prophecy
PhpSpec has recently dropped a 2.0 beta, and it has moved from the mock object framework "Mockery" in favour of a new framework called Prophecy.
The tl;dr guide to upgrading your test suite
- Add
"phpspec/phpspec": "2.0.*@dev"
tocomposer.json
- Change all instances of the
PHPSpec2
namespace toPhpSpec
- Rename all your
spec/<MyClass>
specs tospec/<MyClass>Spec
- Replace the
ANY_ARGUMENT
constant withProphecy\Argument::any()
Having done the above, your test suite should run but you may see some new failures due to the way Prophecy works
- PhpSpec will not let you use methods that are undefined on collaborators
- Stubs in PhpSpec/Prophecy are "all or nothing"
First step – update/install PhpSpec with Prophecy
Documentation is a bit sparse on the ground (but you can contribute) so hopefully I can give you a couple of pointers for upgrading your test suite. I have created an example repository to try and give a quick guide through some of the updates and differences between PhpSpec/Mockery & PhpSpec/Prophecy.
I am assuming you are using composer you can get a fresh copy of PhpSpec by adding
or editing the following dependency in composer.json
"phpspec/phpspec": "2.0.*@dev"
Changes that will break your spec files
At first glance not much has changed, hit the following command and it appears to be business as usual. But if you are trying to run an old test suite then nothing will work.
$ ./vendor/bin/phpspec desc HelloWorld
Take a look at the generated spec and you will notice a couple of changes.
PhpSpec has a new namespace
The namespace PHPSpec2
has changed to PhpSpec
, for example many of your specs will will extend PHPSpec2\ObjectBehavior
, so you will want to update your use statement to use PhpSpec\ObjectBehavior;
.
Spec files/classes now have the suffix "Spec"
If you are upgrading existing specs, you will need to rename your spec for HelloWorld.php
to HelloWorldSpec.php
.
The ANY_ARGUMENT(S) constant has gone
In some of your mock expectations, you may have used something like
$mockObject->methodStub(ANY_ARGUMENT);
Prophecy handles arguments wildcarding slightly differently. If you take a
look at our initial spec again you will see that phpspec desc
generates a spec with a use statement for Prophecy\Argument
. If you want a direct replacement for ANY_ARGUMENT
then you should use:
$mockObject->methodStub(Prophecy\Argument::any());
Changes that can make your specs better
Prophecy integration with PhpSpec contains two key features that give me a lot more confidence in my specs and they may cause your existing test suite to fail.
PhpSpec complains about un-defined methods in collaborators
If you take a look at this commit I have tried to specify that HelloWorld
will say hello to a Person
. With previous versions of PhpSpec this would have worked
however Prophecy will complain:
This is a useful change, for example:
- If you refactor a method name, but don't update a dependant class - it will be caught by the test suite
- If you type hint for an interface, but use methods not defined within that interface your test will fail
Stubs in PhpSpec/Prophecy are "all or nothing"
In Mockery, if you call a method that has not been explictly stubbed it would
return an instance of Mockery\Undefined
. This is problematic, because if you
use loose comparisons, your test suite may behave unexpectedly. If you consider
the following block of code that was added in this commit
// ...
if ($person->isMale()) {
$salutation = 'Mr. ';
} else if ($person->isFemale()) {
$salutation = 'Ms. ';
} else {
// gender in-specific salutation
$salutation = '';
}
// ...
In the spec, we stub $person->isFemale()
and $person->getName()
// test name shortened for brevity
function it_should_address_/*..*/($person)
{
$person->getName()->willReturn('Jane');
$person->isFemale()->willReturn(true);
$this->addressSomeoneWithSalutation($person)
->shouldReturn('Dear Ms. Jane');
}
If you haven't realised yet with Mockery this spec will always fail, but not in
an expected way (or a way that would occur in an actual runtime). The call
to $person->isMale()
will always evaluate to true (because the object Mockery\Undefined
is returned and coerced to true
), incorrectly giving us the salutation for a male.
Prophecy on the other hand will not put up with this, failing the test with a useful message.
[edit] This is because stubs in Prophecy are "loose demand doubles", if you do not stub
any methods on them, they will always return null
. Once you stub a method they
become "strict demand doubles" requiring you to stub all methods that your
SUS is interacting with. 1
We are therefore forced to stub $person->isMale()
in order for our tests to pass
as shown in this commit.
If you have a comment on this post, or if I have missed any of the standout new features then let me know on twitter or email me
[1] thanks to @everzet & @_md for providing explanation on twitter
Follow @peterjmitShare on twitter