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