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

Full Mock Example: the Sequel

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

There's something interesting going on. We're mocking the growFromSpecification() method... but we are not controlling its return value. And, the addDinosaur() method requires a Dinosaur object. So... how is that working? I mean, doesn't a mocked method return null by default? Shouldn't this blow up?

Back in the test, at the bottom, add dump($enclosure->getDinosaurs()->toArray()). Let's see what that looks like!

... lines 1 - 9
class EnclosureBuilderServiceTest extends TestCase
{
public function testItBuildsAndPersistsEnclosure()
{
... lines 14 - 25
dump($enclosure->getDinosaurs()->toArray());
}
}

Run the tests:

./vendor/bin/phpunit

Woh! It holds 2 items... which are mock Dinosaur objects! That's really cool! Thanks to the PHP 7 return type on growFromSpecification(), PHPUnit is smart enough to create a mock Dinosaur and return that, instead of null.

That's not normally a detail you need to think about, but I want you to realize it's happening. We don't really need to, but if we want, we could add ->willReturn(new Dinosaur()).

... lines 1 - 9
... lines 11 - 12
public function testItBuildsAndPersistsEnclosure()
... lines 14 - 17
$dinoFactory->expects($this->exactly(2))
... line 19
->willReturn(new Dinosaur())
... lines 21 - 27
}
... lines 29 - 30

This time, the dump() from the test shows real Dinosaur objects. Rawr! Take the dump out of the test.

The Bug: Unsaved Enclosure

Ok, there's one more bug hiding inside EnclosureBuilderService. The whole point of the method is that we can call it and it will create the Enclosure and save it to the database. But look! That never happens! We inject the entity manager... and then... never use it! Whoops!

The return value of this method is correct... but really... we also care that the method did something else. We want to guarantee that persist() and flush() are called.

Back in the test, add $em->expects($this->once()) with ->method('persist'). We know that this should be called with an instance of an Enclosure object. We don't know exactly which Enclosure object, but we can check the type with $this->isInstanceOf(Enclosure::class).

... lines 1 - 13
public function testItBuildsAndPersistsEnclosure()
{
... lines 16 - 17
$em->expects($this->once())
->method('persist')
->with($this->isInstanceOf(Enclosure::class));
... lines 21 - 33
}
... lines 35 - 36

Try the test!

./vendor/bin/phpunit

There's the failure: persist should be called 1 time, but was called 0 times.

Back in Enclosurebuilder, add $this->entityManager->persist($enclosure).

... lines 1 - 9
class EnclosureBuilderService
{
... lines 12 - 30
public function buildEnclosure(
... lines 32 - 34
{
... lines 36 - 41
$this->entityManager->persist($enclosure);
... lines 43 - 44
}
... lines 46 - 67
}

Of course, the flush() call is still missing. In the test, check for that: $em->expects($this->atLeastOnce())->method('flush').

... lines 1 - 11
class EnclosureBuilderServiceTest extends TestCase
{
public function testItBuildsAndPersistsEnclosure()
{
... lines 16 - 21
$em->expects($this->atLeastOnce())
->method('flush');
... lines 24 - 36
}
}

You could also use $this->once()... calling flush() multiple times isn't a problem... but it is a bit wasteful. Make sure the test fails before we fix it:

./vendor/bin/phpunit

It does. In the builder, add $this->entityManager->flush() and then... run the tests. They pass!

... lines 1 - 9
class EnclosureBuilderService
{
... lines 12 - 30
public function buildEnclosure(
... lines 32 - 34
{
... lines 36 - 43
$this->entityManager->flush();
... lines 45 - 46
}
... lines 48 - 69
}

Thanks to mocking, we just created a killer test. Just remember: if the object you need is a service, mock it. If it's a simple model object, that's overkill: just create the object normally.

Leave a comment!

7
Login or Register to join the conversation
Default user avatar
Default user avatar manhee | posted 4 years ago | edited

There are couple of problems:
First: you don't set to which enclosure Dinosaur belongs:
add setEnclosure($enclosure) to Dinosaur Entity

Second: there may be thrown NotABuffetException later with integration tests because you draw Dinosaur diet in EnclosureBuilderService and different species may be in same enclosure. I solved it like this

`
private function addDinosaurs(int $numberOfDinosaurs, Enclosure $enclosure): void
{

$diet = ['herbivore', 'carnivorous'][random_int(0, 1)];

for ($i = 0; $i < $numberOfDinosaurs; $i++) {
    $length = ['small', 'large', 'huge'][random_int(0, 2)];
    $specification = "{$length} {$diet} dinosaur";
    $dinosaur = $this->dinosaurFactory->growFromSpecification($specification);
    $dinosaur->setEnclosure($enclosure);

    $enclosure->addDinosaur($dinosaur);
}

}
`

Reply

Hey manhee

Thanks for your feedback, You are actually right on the second case, the test may fail because the implementation is wrong, it randomly selects which diet the dinosaur will have. I'll talk about this problem internally and come up with a solution

About first case. I don't think it's needed to set the inverse side of the relationship but either is wrong to set it up. So, it's up to your use case.

Cheers!

Reply

Yep, nice catch on the randomness flaw! As Diego mentioned, the setEnclosure() call (which is actually the owning side - Diego has that part slightly off) is called already when we set the inverses side - the addDinosaur() method has that logic internally - it’s just a nice thing the make:entity command gives you. But let me know if I’m missing a detail!

Cheers!

Reply
Francois Avatar

I don't understand 2 things:
- why is the exception not thrown? I tried with 1000 dinosaurs, it always pass. Although if I'm right, the enclosure and dinosaurs are a real objects, so the exception should be thrown, no?
- how could we rewrite the test so that it fail because of this randomness error (I mean, how could the test fail before we add the correction manhee?

Reply

Hey Francois,

The test should fail at some point, if you check the implementation of EnclosureBuilderService::addDinosaurs() it's possible to add dinosaurs with different diets and trigger the exception.

how could we rewrite the test so that it fail because of this randomness error (I mean, how could the test fail before we add the correction manhee?

I don't think that's a good idea because you're working with random data, unless you install a library to manipulate the built-in array_rand() function and mock it's return value, then your test won't be reliable. The solution is to fix the production code and leave the test as it is

Cheers!

Reply
Francois Avatar

Hey Diego, thanks a lot

No really, it doesn't fail. Did you try?
And, about having the test testing this specific potential error, I think it could be tested independantly and we have a hole in the coverage. But:

Unfortunately it's not important for me anymore, sorry, I'm on something else now, so I won't be able to come back on this deep enough..
thanks!

Reply

Hey, to be honest I didn't run the code but by analyzing it you can see the diet it's chosen randomly every time you call addDinosaur(), so, if you call that method many times it should fail eventually. Anyways, that's a flaw in the implementation and my recomendation is, instead of adding a test for it, just fix the bug and keep the current test

Cheers!

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