Search code examples
phplaravelphpspec

Mocking new object creation with PhpSpec in Laravel


I have a class called Campaign that is responsible for booking single campaign in the external API. I have EntryBooking class that is responsible for preparing the entry and booking it using the Campaign class. There might be instances where I want to create more than one campaign so for each campaign I want to create a new Campaign object and call book() method on it. (Each campaign has its own Campaign object)

The issue I'm having is that I want to unit test the EntryBooking class and I want to mock Campaign objects.

I am using the BenConstable/phpspec-laravel package so I have access to facades in my specs.

I am trying to do it like this:

# EntryBookingSpec.php
function it_should_book_campaigns_for_entry(Entry $entry, Campaign $campaignMock)
{
    $campaignMock->book()->shouldBeCalled();
    App::instance(Campaign::class, $campaignMock);

    $this->bookForEntry($entry);
}

-

# EntryBooking.php
class EntryBooking
{
    public function bookForEntry(Entry $entry): void
    {
        $campaign = App::make(Campaign::class);
        // do the processing and set values for $campaign
        $campaign->book();
    }
}

I am trying with App::instance() because in live environment, the App::make() will create a new instance each time it's called, but while testing I want it to return the very same object so I can make my assertions on it.

The problem is that the predictions fail. Even though I call $campaignMock->book()->shouldBeCalled(); and in tested class I call $campaign->book() I still get:

some predictions failed:
    Double\vendor\package\Campaign\P1:
      No calls have been made that match:
        Double\vendor\package\Campaign\P1->book()
      but expected at least one.

Solution

  • It turns out that when you use Dependency Injection using constructors PhpSpec will automatically create a mocked object from your MethodProphecy, but if you want your service container to return mocked object you need to pass it a wrapped object on your own. So changing the following

    App::instance(Campaign::class, $campaignMock);
    

    to

    App::instance(Campaign::class, $campaignMock->getWrappedObject());
    

    Makes everything work as expected.

    EDIT

    Or even better, mock the Service Container completely:

    App::shouldReceive('make')
        ->with(Campaign::class)
        ->andReturn($campaignMock->getWrappedObject());
    

    And call \Mockery::close() in your letGo() function. There is no way for the class to receive concrete class implementations and you actually test if the class made resolve requests to the Service Container in the first place.