Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

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!

26
Login or Register to join the conversation
Default user avatar
Default user avatar Radoje Albijanic | posted 5 years ago | edited

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.

14 Reply

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:


&lt;!-- phpunit.xml.dist --&gt;
&lt;!-- ... --&gt;

    &lt;php&gt;
         &lt;!-- ... --&gt;
         &lt;env name="SYMFONY_PHPUNIT_REMOVE" value="symfony/yaml" /&gt;
    &lt;/php&gt;

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!

2 Reply

Hey Radoje Albijanic

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

Cheers!

Reply
Nils L. Avatar
Nils L. Avatar Nils L. | MolloKhan | posted 3 years ago | edited

MolloKhan No note in the video ... :)

1 Reply

Hey Nils L.

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!

Reply
Nils L. Avatar

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

Reply

Hi,
FYI - incorrect video filename

Reply

Forget :) we already checked it! Thanks for the reporting!

Cheers!

Reply

Hello Mepcuk

Give me please more details what is wrong with it?

Cheers!

Reply
Gonzalo G. Avatar
Gonzalo G. Avatar Gonzalo G. | posted 2 years ago | edited

Hello there , a question, how would the test of a method like the one I show below be approached:

public function execute(array $terms_array): array {
$items = [];
$base_uri = $this->getSetting('base_url');
foreach ($terms_array as $term_item) {
$items[] = $this->getPeople($term_item, $base_uri);
}
return array_merge([], ...$items);
}

Any suggestions on how it would be done?

Reply

Hey Gonzalo G.

I think I'd test only the state of what it returns instead of the algorithm because it calls a few other methods. I'd pass an input that I already know what the output should be and assert that

Cheers!

Reply
Todor Avatar
Todor Avatar Todor | posted 3 years ago | edited

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

It works:

<br />$this->prophesize(...);<br />

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!

Reply

Hey Todor!

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!

Reply

Hi again weaverryan!

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:


&lt;!-- phpunit.xml.dist --&gt;
&lt;!-- ... --&gt;

    &lt;php&gt;
         &lt;!-- ... --&gt;
         &lt;env name="SYMFONY_PHPUNIT_REMOVE" value="symfony/yaml" /&gt;
    &lt;/php&gt;

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!

Reply

Hey Todor

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!

Reply

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)?
Reply

Hey alexchromets

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

Cheers!

1 Reply
_zippp Avatar

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

Reply

Hey _zippp !

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!

Reply
Marcel D. Avatar
Marcel D. Avatar Marcel D. | posted 4 years ago

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).

Reply

Hey Marcel D.

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

Cheers!

1 Reply
Default user avatar
Default user avatar Matt | posted 4 years ago | edited

Found two typos:

Missing closing bracket in`
$em->persist(Argument::type(Enclosure::class)`

Missing closing bracket in`
->growFromSpecification(Argument::type('string')`

Reply

Hey @Matt!

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

Cheers!

Reply
Default user avatar
Default user avatar toporovvv | posted 5 years ago

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);

Reply

Thanks toporovvv!
I just fixed it :)

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

While the fundamentals of PHPUnit haven't changed, this tutorial *is* built on an older version of Symfony and PHPUnit.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.0, <7.4",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^1.6", // 1.10.3
        "doctrine/orm": "^2.5", // v2.7.2
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "sensio/distribution-bundle": "^5.0.19", // v5.0.21
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.28
        "symfony/monolog-bundle": "^3.1.0", // v3.1.2
        "symfony/polyfill-apcu": "^1.0", // v1.6.0
        "symfony/swiftmailer-bundle": "^2.3.10", // v2.6.7
        "symfony/symfony": "3.3.*", // v3.3.13
        "twig/twig": "^1.0||^2.0" // v2.4.4
    },
    "require-dev": {
        "doctrine/data-fixtures": "^1.3", // 1.3.3
        "doctrine/doctrine-fixtures-bundle": "^2.3", // v2.4.1
        "liip/functional-test-bundle": "^1.8", // 1.8.0
        "phpunit/phpunit": "^6.3", // 6.5.2
        "sensio/generator-bundle": "^3.0", // v3.1.6
        "symfony/phpunit-bridge": "^3.0" // v3.4.30
    }
}
userVoice