Partial Mocking
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 one more cool thing I want to show you with integration tests. It's kind of a weird, unnatural mixture of unit tests and integration tests. I love it! I call it partial mocking.
Right now, we're fetching the EnclosureBuilderService
from the container. But there's another option. Instead, create it manually - new EnclosureBuilderService()
- and pass in the dependencies manually. For example, pass $this->getEntityManager()
as the first argument.
// ... lines 1 - 13 | |
class EnclosureBuilderServiceIntegrationTest extends KernelTestCase | |
{ | |
// ... lines 16 - 22 | |
public function testItBuildsEnclosureWithDefaultSpecifications() | |
{ | |
// ... lines 25 - 32 | |
$enclosureBuilderService = new EnclosureBuilderService( | |
$this->getEntityManager(), | |
// ... line 35 | |
); | |
// ... lines 37 - 56 | |
} | |
// ... lines 58 - 73 | |
} |
Why would we do this? It seems like more work! Because... if it's useful, we could mock certain dependencies. Yep! Instead of fetching the DinosaurFactory
from the container, mock it: $dinoFactory = $this->createMock(DinosaurFactory::class)
.
// ... lines 1 - 13 | |
class EnclosureBuilderServiceIntegrationTest extends KernelTestCase | |
{ | |
// ... lines 16 - 22 | |
public function testItBuildsEnclosureWithDefaultSpecifications() | |
{ | |
// ... lines 25 - 27 | |
$dinoFactory = $this->createMock(DinosaurFactory::class); | |
// ... lines 29 - 56 | |
} | |
// ... lines 58 - 73 | |
} |
Then, $dinoFactory->expects($this->any())
- we don't really care - with ->method('growFromSpecification')
and ->willReturn(new Dinosaur())
.
// ... lines 1 - 22 | |
public function testItBuildsEnclosureWithDefaultSpecifications() | |
{ | |
// ... lines 25 - 28 | |
$dinoFactory->expects($this->any()) | |
->method('growFromSpecification') | |
->willReturn(new Dinosaur()); | |
// ... lines 32 - 56 | |
} | |
// ... lines 58 - 75 |
Pass this as the second argument. This isn't particularly useful in this situation. But sometimes, being able to mock - and control - one or two dependencies in an integration test is really awesome!
// ... lines 1 - 22 | |
public function testItBuildsEnclosureWithDefaultSpecifications() | |
{ | |
// ... lines 25 - 32 | |
$enclosureBuilderService = new EnclosureBuilderService( | |
$this->getEntityManager(), | |
$dinoFactory | |
); | |
// ... lines 37 - 56 | |
} | |
// ... lines 58 - 75 |
Ok, try the test!
./vendor/bin/phpunit --filter testItBuildsEnclosureWithTheDefaultSpecification
Oh man! It fails! Weird! There is only 1 Dinosaur... but there should be 3! What's going on? This is subtle: PhpUnit is smart enough to take this one dinosaur object and return it each time growFromSpecification()
is called. But to Doctrine, it looks like we're asking it to save the same one Dinosaur
object: not three separate dinosaurs. The result would be a less than thrilling theme park.
The fix is to change this to willReturnCallback()
and pass it a function. This will be called each time growFromSpecification()
is called. And since it is passed a $specification
argument, the callback also receives this. It's the best way to return different values based on the arguments.
// ... lines 1 - 22 | |
public function testItBuildsEnclosureWithDefaultSpecifications() | |
{ | |
// ... lines 25 - 28 | |
$dinoFactory->expects($this->any()) | |
// ... line 30 | |
->willReturnCallback(function($spec) { | |
// ... line 32 | |
}); | |
// ... lines 34 - 58 | |
} | |
// ... lines 60 - 77 |
We don't need that in this case. We'll just say return new Dinosaur()
. And that's it! Try the tests again.
// ... lines 1 - 22 | |
public function testItBuildsEnclosureWithDefaultSpecifications() | |
{ | |
// ... lines 25 - 28 | |
$dinoFactory->expects($this->any()) | |
// ... line 30 | |
->willReturnCallback(function($spec) { | |
return new Dinosaur(); | |
}); | |
// ... lines 34 - 58 | |
} | |
// ... lines 60 - 77 |
./vendor/bin/phpunit --filter testItBuildsEnclosureWithTheDefaultSpecification
We got it! So integration tests are one of my favorite, favorite, favorite things because they're very pragmatic. When I think about testing a class, here's the logic I follow:
Does this class scare me?
If the logic is simple, I don't test it. I will still make sure the new feature works through manual testing. And often, that's enough.Can I unit test this class?
If I mock all of the dependencies, is the scary logic still testable? ForDinosaurFactory
, the answer was yes: we mocked theDinosaurLengthDeterminator
, but we were still able to test that the specification string is parsed correctly to create carnivorous and non-carnivorous dinosaurs.If the class cannot be unit tested, then I write an integration test.
This is pretty common in my apps: each individual "unit" is often pretty simple. But when you integrate them all together, that's scary enough to need a test.
Ok, on to functional testing!
Hi Sir, i followed all the step in this chapter but i'm having a warning when i execute this command :
<br />./vendor/bin/simple-phpunit --filter testItBuildsEnclosureWithDefaultSpecification<br />
i have this warning
`
PHPUnit 7.4.5 by Sebastian Bergmann and contributors.
Testing
W 1 / 1 (100%)
Time: 365 ms, Memory: 10.00 MB
There was 1 warning:
1) Tests\AppBundle\Service\EnclosureBuilderServiceIntegrationTest::testItBuildsEnclosureWithDefaultSpecification
Trying to configure method "growFromSpecification" which cannot be configured because it does not exist, has not been specified, is final, or is static
WARNINGS!
Tests: 1, Assertions: 0, Warnings: 1.
`
Please how can i fix it ?
Thank you.