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.

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 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:

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

  2. Can I unit test this class? If I mock all of the dependencies, is the scary logic still testable? For DinosaurFactory, the answer was yes: we mocked the DinosaurLengthDeterminator, but we were still able to test that the specification string is parsed correctly to create carnivorous and non-carnivorous dinosaurs.

  3. 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!

Leave a comment!

  • 2019-10-14 Diego Aguiar

    Aha! That's an easy mistake to do :)

  • 2019-10-12 william bridge

    Thank you very much Sir for helping me fix it. It was a typo, i wanted to mock "DinosaurFactory" instead of "Dinosaur".

  • 2019-10-11 Diego Aguiar

    Hey william bridge

    Can you double check the method DinosaurFactory::growFromSpecification does not violate any of the rules above mentioned? And just in cast, double check that you are trying to mock the right class of DinosaurFactory (just check its namespace)

    Cheers!

  • 2019-10-11 william bridge

    Hi Sir, i followed all the step in this chapter but i'm having a warning when i execute this command :

    ./vendor/bin/simple-phpunit --filter testItBuildsEnclosureWithDefaultSpecification

    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.

  • 2019-05-07 weaverryan

    Hey Argon!

    Ha! I'm glad you like it the style :). It's a bit of a trick... through hard work. We record my rough audio while recording the video, transcribe that to text, clean up that text and *then* I re-record the final audio (and of course, then we splice it all together). That allows us to have nice, "clean" text. But because the text originated from normal conversation from the original video recording, it (hopefully) sounds natural.

    Cheers!

  • 2019-05-01 Argon

    I'd like to see how these screencast voice is being recorded, they are such emotional :D