Mocking with Prophecy

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

PHPUnit has a mocking system. But it's not the only mocking library available. There are two other popular ones: Mockery & Prophecy. They all do the same thing, but each has its own feel.

I really like Prophecy, and it comes with PHPUnit automatically! So let's redo the EnclosureBuilderTest with Prophecy to see how it feels.

Tip

If you installed PHPUnit by installing symfony/phpunit-bridge, then you need to add one line to your phpunit.xml.dist file to tell the bridge that you want prophecy:

<!-- phpunit.xml.dist -->
<!-- ... -->

    <php>
        <!-- ... -->
        <!-- tells phpunit-bridge that you do *not* want to remove prophecy -->
        <env name="SYMFONY_PHPUNIT_REMOVE" value="" />
    </php>

Create a new class called EnclosureBuilderServiceProphecyTest. It will extend the normal TestCase and we can give it the same method: testItBuildsAndPersistsEnclosure().

... lines 1 - 12
class EnclosureBuilderServiceProphecyTest extends TestCase
{
public function testItBuildsAndPersistsEnclosure()
{
... line 17
}
}

Mocking Prophecy Style

Let's translate the PHPUnit mock code into Prophecy line-by-line. To create the EntityManager mock, use $this->prophesize(EntityManagerInterface::class). That's pretty similar.

... lines 1 - 12
class EnclosureBuilderServiceProphecyTest extends TestCase
{
public function testItBuildsAndPersistsEnclosure()
{
$em = $this->prophesize(EntityManagerInterface::class);
}
}

Next, we need to assert that persist() will be called once() and that it is passed an Enclosure object. This is where things get different... and pretty fun... Instead of thinking of $em as a mock, pretend it's the real object. Call $em->persist(). To make sure this is passed some Enclosure object, pass Argument::type(Enclosure::class).

... lines 1 - 14
public function testItBuildsAndPersistsEnclosure()
{
... lines 17 - 18
$em->persist(Argument::type(Enclosure::class))
... line 20
}
... lines 22 - 23

We'll talk more about how these arguments work in a minute. Then, because we want this to be called exactly once, add shouldBeCalledTimes(1).

... lines 1 - 14
public function testItBuildsAndPersistsEnclosure()
{
... lines 17 - 18
$em->persist(Argument::type(Enclosure::class))
->shouldBeCalledTimes(1);
}
... lines 22 - 23

Oh, and notice that I am not getting auto-completion. That's because Prophecy is a super magic library, so PhpStorm doesn't really know what's going on. But actually, there are two amazing PhpStorm plugins that - together - will give you auto-completion for Prophecy... and many other things. They're called "PHP Toolbox" and "PHPUnit Enhancement". I learned about these so recently, that I didn't have them installed yet for this tutorial. Thanks for the tip Stof!

Next, we need to make sure flush() is called at least once. That's easy: $em->flush()->shouldBeCalled().

... lines 1 - 14
public function testItBuildsAndPersistsEnclosure()
{
... lines 17 - 21
$em->flush()->shouldBeCalled();
}
... lines 24 - 25

Don't you love it? In addition to shouldBeCalledTimes() and shouldBeCalled(), there is also shouldNotBeCalled() and simply should(), which accepts a callback so you can do custom logic.

Mocking the DinosaurFactory

Let's keep moving: add the DinosaurFactory with $dinoFactory = $this->prophesize() and DinosaurFactory::class.

... lines 1 - 14
public function testItBuildsAndPersistsEnclosure()
{
... lines 17 - 23
$dinoFactory = $this->prophesize(DinosaurFactory::class);
}
... lines 26 - 27

Here, we need to make sure that the growFromSpecification method is called exactly two times with a string argument and returns a dinosaur. Ok! Start with $dinoFactory->growFromSpecification().

Here's how the arguments part really works. If you don't care what arguments are passed to the method, just leave this blank. But if you do care, then you need to pass all of the arguments here, as if you were calling this method.

For example, imagine the method accepts three arguments. If we passed foo, bar, baz here, this would make sure that the method was called with exactly these three args.

Our situation is a bit trickier: we don't know the exact argument, we only know that it should be a string. To check that, use Argument::type('string').

... lines 1 - 14
public function testItBuildsAndPersistsEnclosure()
{
... lines 17 - 25
$dinoFactory
->growFromSpecification(Argument::type('string'))
... lines 28 - 29
}
... lines 31 - 32

There are a few other useful methods on this Argument class. The most important is Argument::any(). You'll need this if you want to assert that some of your arguments match a value, but you don't care what value is passed for the other arguments.

The most powerful is that(), which accepts an all-powerful callback as an argument.

Next, this method should be called 2 times. No problem: ->shouldBeCalledTimes(2). And finally, it should return a new Dinosaur object. And that's the same as in PHPUnit: ->willReturn(new Dinosaur()). The other 2 useful functions are willThrow() to make the method throw an exception and will(), which accepts a callback so you can completely control the return value.

... lines 1 - 14
public function testItBuildsAndPersistsEnclosure()
{
... lines 17 - 25
$dinoFactory
->growFromSpecification(Argument::type('string'))
->shouldBeCalledTimes(2)
->willReturn(new Dinosaur());
}
... lines 31 - 32

And... yea! That's it! I'll copy the rest of the test and paste it. Re-type the e on EnclosureBuilderService to add the use statement on top.

... lines 1 - 7
use AppBundle\Service\EnclosureBuilderService;
... lines 9 - 12
class EnclosureBuilderServiceProphecyTest extends TestCase
{
public function testItBuildsAndPersistsEnclosure()
{
... lines 17 - 30
$builder = new EnclosureBuilderService(
$em->reveal(),
$dinoFactory->reveal()
);
$enclosure = $builder->buildEnclosure(1, 2);
$this->assertCount(1, $enclosure->getSecurities());
$this->assertCount(2, $enclosure->getDinosaurs());
}
}

Revealing the Prophecy

There's one other tiny difference in Prophecy. First, I'll break this onto multiple lines so it looks nicer. When you finally pass in your mock, you need to call ->reveal(). On a technical level, this kind of turns your "mock builder" object into a true mock object. On a philosophical Prophecy level, this reveals the prophecy that the prophet prophesized.

Fun, right? If that made no sense - it's ok! The Prophecy documentation - while being a little strange - is really fun and talks a lot more about dummies, stubs, prophets and other things. If you're curious, it's worth a read.

Ok, that should be it! Find your terminal and run the test:

./vendor/bin/phpunit

They pass! Right on the first try. So that's Prohecy: it's a bit more fun than PHPUnit and is also quite popular. If you like it better, use it!

Next, there are many options you can pass to the phpunit command. Let's learn about the most important ones... so that we can ignore the rest.

Leave a comment!

  • 2020-07-08 weaverryan

    Hi!

    Just an update on this old problem :). First, sorry for anyone who it it - we should have added a clear note much earlier. Second, the reason that you don't have Prophecy when you use symfony/phpunit-bridge is that it specifically *removes* it. It does that for some historical reasons that aren't important. But, you can tell the phpunit-bridge that you *do* want Prophecy by adding one line to your phpunit.xml.dist file:


    <!-- phpunit.xml.dist -->
    <!-- ... -->

    <php>
    <!-- ... -->
    <env name="SYMFONY_PHPUNIT_REMOVE" value="symfony/yaml" />
    </php>

    If you do this, you should *not* need to install phpspec/prophecy directly in your app: it will be included normally when the phpunit-bridge downloads phpunit.

    Cheers!

  • 2020-07-08 weaverryan

    Hi again Todor Vachkov!

    After my research, I've found the problem (and the correct note to add). You should *not* need to install prophecy manually. That *may* work, but phpunit *already* comes with prophecy. The reason that you do not see it is that, when simple-phpunit installs PHPUnit, it *removes* Prophecy from the install. It does this for some historical reasons related to dependencies. But if you *do* want prophecy, you can enable it via your phpunit.xml.dist file:


    <!-- phpunit.xml.dist -->
    <!-- ... -->

    <php>
    <!-- ... -->
    <env name="SYMFONY_PHPUNIT_REMOVE" value="symfony/yaml" />
    </php>

    That tells the phpunit-bridge that it's ok to remove symfony/yaml (something it does in older versions) but it should not remove prophecy. Try that and let us know how it works!

    Cheers!

  • 2020-07-08 weaverryan

    Hey Todor Vachkov!

    Yikes! We have a system in place to get notes added to tutorials, but we somehow dropped the ball on this one! My apologies for the trouble :/. As Vladimir mentioned, this could be a PHP 7.4 problem, or it could be something else. If you want to post the dependency error conflict, we can help manager it.

    In the mean time, we'll get the note added (we need to play with the code a bit first to make sure we get the note just right).

    Cheers!

  • 2020-07-07 Vladimir Sadicov

    Hey Todor Vachkov

    This course is a little bit old and it doesn't support php 7.4. you can see this requirement in composer.json file. We are trying to improve code delivery to be more strict you can see a tooltip on "Download > Courser code" dropdown menu, but yeah it's just a text tooltip and composer still allows installation.

    BTW Course code works great on php 7.3 ;) However we can't guarantee that it will work if you update packages to latest version. As I can see you are using PHPUnit 8, but course is based on PHPUnit 6 there can be a lot of differences in code but main principles of testing are same.

    Cheers!

  • 2020-07-06 Todor Vachkov

    Somehow my PhpStorm doesn't fully autocomplete the Prophecy part.

    It works:


    $this->prophesize(...);

    It doesn't works:


    // 1) Argument remains as an undefinied class
    // 2) shouldBeCalledTimes is not found too

    $em->persist(Argument::type(...))->shouldBeCalledTimes(...)

    My Setup is as follows:

    PHP 7.4.5
    PhpStorm 2020.1.2
    PHPUnit 8.3
    PHP Toolbox 5.1.1
    PHPUnit Enhancement 4.1

    Anything special you have to do for prophecy support? It isn't working out of the box for me.

    Update: well it seems that I don't have the Prophecy classes at all.
    $this->prophesize(...); throws Class 'Prophecy\Prophet' not found.
    Since I am using the course code with phpunit-bridge. and that points out again to the very first comment from Radoje Albijanic about the missing note.
    After two years the note is still missing :D

    Well though when I try to add prophecy manually
    composer require phpspec/prophecy

    I get a bunch of dependencies conflicts.

    Cheers!

  • 2020-02-24 Vladimir Sadicov

    Hey Alex Khromets

    Good catch, interesting how it happend :( however we just fixed this code block, so keep eye and feel free to report such cases!

    Cheers!

  • 2020-02-23 Alex Khromets

    Why there are so many errors in this course?
    After 2 years, did you find that you have an error code in the last example in this video?

    $em->persist(Argument::type(Enclosure::class)
    ->shouldBeCalledTimes(1);


    $dinoFactory
    ->growFromSpecification(Argument::type('string')
    ->shouldBeCalledTimes(2)
    ->willReturn(new Dinosaur());

    Where is the closing parenthesis (after Argument definition)?

  • 2020-01-29 Jens Wodrich

    sure .. as recommended i use symfony's PHPUnit Bridge "symfony/phpunit-bridge": "^4.0.5".
    that installed "phpunit/phpunit": "^7.5"

  • 2020-01-29 Vladimir Sadicov

    Hey Jens Wodrich

    Can you provide some more info about why it needed? Are you running phpunit directly? or vie Symfony's PHPUnit bridge? Which version of PHPUnit do you have?

    Cheers!

  • 2020-01-29 Jens Wodrich

    Diego Aguiar No note in the video ... :)

  • 2019-09-23 weaverryan

    Hey Iuli Dercaci !

    Sorry for the slow reply! We were checking into this. Prophecy comes with PHPUnit automatically... so it's interesting that your app was missing them. What version of PHPUnit are you using ./vendor/bin/phpunit --version? And are you using PHPUnit directly (i.e. you run it via ./vendor/bin/phpunit) or are you using it via Symfony's PHPUnit bridge (i.e. you run it with php bin/phpunit)? Let us know - I'd love to add a note to the tutorial if we need one!

    Cheers!

  • 2019-09-11 Iuli Dercaci

    seems like the libraries used in the tutorial a bit dated, for those who's got "Prophecy" classes not found error you need to add it to your project vendors:
    composer require phpspec/prophecy

  • 2019-03-11 Diego Aguiar

    Hey Marcel dos Santos

    Thanks for informing us about it. We really care about our quality :)

    Cheers!

  • 2019-03-09 Marcel dos Santos

    There is a typo in subtitles at 0:58 second. It should be use $em = $this->prophesize(EntityManagerInterface::class) instead of use $this->em->prophesize(EntityManagerInterface::class).

  • 2018-11-07 Diego Aguiar

    Fixed now!

  • 2018-11-06 weaverryan

    Hey @Matt!

    Nice catch! We're going to update those ASAP! Thanks for letting us know!

    Cheers!

  • 2018-11-02 Matt

    Found two typos:

    Missing closing bracket in

    $em->persist(Argument::type(Enclosure::class)


    Missing closing bracket in

    ->growFromSpecification(Argument::type('string')
  • 2018-04-19 Diego Aguiar

    Thanks toporovvv!
    I just fixed it :)

  • 2018-04-19 toporovvv

    There are two typos here:
    $this->em->prophesize(EntityManagerInterface::class) - em is redundant here.

    And here closing bracket was missed:
    $em->persist(Argument::type(Enclosure::class)
    ->shouldBeCalledTimes(1);

  • 2018-03-09 Diego Aguiar

    Hey Radoje Albijanic

    Thanks for informing us about it. We will add a note, so no one else get confused :)

    Cheers!

  • 2018-03-09 Radoje Albijanic

    Hey folks!

    If you installed `symfony/phpunit-bridge` like suggested in the first video, you will have to install prophecy too, I didn't have prophecy classes until run:


    composer require phpspec/prophecy

    Cheers.