Search code examples
laravel-5phpunitlaravel-artisanmockery

Using chained partial mocks on an interface used in an artisan command


I'm trying to unittest an artisan command in Laravel 5.3. The command calls on functions in a class that is provided to the command constructor as an interface. That interface calls on functions in another class. This is the general setup.

class MyCommand
{
    public function __construct(MyRepositoryInterface $interface)
    {
        ...
        $this->interface = $interface;
        ...
    }

    public function fire()
    {
        $this->interface->useTheSecondClass();
    }
}

class MyRepository implements MyRepositoryInterface
{
    public function __construct(MySecondRepositoryInterface $second)
    {
        ...
        $this->second = $second;
        ...
    }

    public function useTheSecondClass()
    {
        $response = $this->second->getSomeValue();
    }
}

class MySecondRepository implements MySecondRepositoryInterface
{
    /**
     * @return Some\External\Client
     */
    public function getExternalClient()
    {
        ....
        return $external_client;
    }

    public function getSomeValue()
    {
        $client = $this->getExternalClient();

        $something = $client->doSomething();

        Event::fire('some event based on $something`);

        return $something;
    }
}

I'm attempting to mock the variable returned in MySecondRepository -> getExternalClient() so that I can fake an external API call and use that faked data to test both the MySecondRepository -> getSomeValue() and MyRepository -> useTheSecondClass() functionalities as called from the MyCommand class as such.

public function testMyCommand()
{
    $external_client_mock = Mockery::mock("Some\External\Client");
    $external_client_mock->shouldReceive("doSomething")
        ->andReturn("some values");

    $second_repository_mock = Mockery::mock("MySecondRepositoryInterface")
        ->makePartial();
    $second_repository_mock->shouldReceive("getExternalClient")
        ->andReturn($external_client_mock);

    $resource = new MyRepository($second_repository_mock);
    $this->app->instance("MyRepositoryInterface", $resource);

    $class = App::make(MyCommand::class);
    $class->fire();

    ...
}

I have used this exact same mock chain successfully to test the $resource variable directly (e.g., testing $resource->useTheSecondClass() directly, not through MyCommand), but in this situation, while $second_repository_mock->getExternalClient() is mocking correctly, the test is still expecting there to be a mocked expectation for $second_repository_mock->getSomeValue(). Since $second_repository_mock is set to a partial mock, I don't understand why it's still looking for all functions to be mocked.

If I remove the $external_client_mock part and fully mock $second_repository_mock my tests directly related to the artisan command work, however I'd like to test that the event triggered in getSomeValue() is dealt with properly from the artisan command, which I can't do if I can't use the partial mock.

Does anyone have any insight on why this isn't working?


Solution

  • You are trying to mock an interface which makes no sense. Interfaces have no concrete implementations to use. This results in the faulty code. Just mock your repositories and it will work.
    EDIT
    Just ran the tests with the following refactor and they went to green:

    public function testMyCommand()  // phpcs:ignore
    {
        $external_client_mock = \Mockery::mock("App\Client");
        $external_client_mock->shouldReceive("doSomething")
            ->andReturn("some values");
    
        $second_repository_mock = \Mockery::mock("App\MySecondRepository[getExternalClient]")
            ->shouldIgnoreMissing();
        $second_repository_mock->shouldReceive("getExternalClient")
            ->andReturn($external_client_mock);
    
        $resource = new MyRepository($second_repository_mock);
        $this->app->instance("App\MyRepositoryInterface", $resource);
    
        $class = \App::make(\App\MyClass::class);
        $class->fire();
    }
    

    The only major difference is that you had App\MyCommand in the second to last line and not App\MyClass.