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.
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.
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.
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
{
}
`