If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThere'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.
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.
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!
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!
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?
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!
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!
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!
// 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
}
}
There are couple of problems:
First: you don't set to which enclosure Dinosaur belongs:
add
setEnclosure($enclosure)
to Dinosaur EntitySecond: 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
{
}
`