Reputation: 5422
A brief introduction to give you an idea of what I want to achieve…
I have a Laravel 5.6 artisan command that calls an external API with a simple GET request to get some data. I want to test this without actually calling any API physically. (I want to be offline and still have test test green).
Now a brief explanation of how the logic is arranged.
1) This artisan command (php artisan export:data
) has its own constructor (__construct
) where I inject bunch of stuff. One of the things I inject is the ExportApiService
class. Easy.
public function __construct(
ExportApiService $exportApiService,
) {
parent::__construct();
$this->exportApiService = $exportApiService;
}
2) Now ExportApiService
extends abstract class AbstractApiService
. Inside this abstract class I made a constructor where I simply inject GuzzleHttp\Client
in a $client
property so that in ExportApiService
I can call API by using $this->client
. Easy.
3) So my ExportApiService
has a method called exportData
. Trivial. Works.
public function exportData(array $args = []): Collection
{
$response = $this->client->request('GET', 'https://blahblah.blah');
return collect(json_decode($response->getBody()->getContents(), true));
}
4) So finally in my artisan command (that I introduced in point no. 1) I call:
public function handle()
{
$exportedData = $this->exportApiService->exportData($this->args);
$this->info('Exported ' . count($exportedData) . ' records');
}
I want to test if the output matches my expectations without calling that external API at all. I want to mock it somehow…
public function testExportSuccessful()
{
$this->console->call('export:data', [$some_arguments]);
$output = $this->console->output();
$this->assertContains('Exported 123 records', $output); // this does not work :(
}
Now my exact question - how can I force Laravel / PHPUnit to return a fixed value every time the artisan command will call exportData()
?
What did you try?
I tried creating a setUp()
method with the following code:
public function setUp()
{
parent::setUp();
$mock = $this->createMock(ExportApiService::class);
$mock->method('exportData')->willReturn(123); // give me a dummy integer each time exportData is being called so that I can use is in the assert at the end
}
But this does not work at all however it makes sense to me.
Thank you for any help.
Upvotes: 1
Views: 2430
Reputation: 2203
You created a mock, but this mock never reaches your code. I just lies around. To make sure it reaches your code you must understand what happens when you put an class in your constructor.
When you put a class in your construct, Laravel asks the ioc container for an instance of the requested class. If is not available it will try to generate it for you. That is where you can get your mock in.
Basically all you have to do is to let Laravel know which instance should be used when the specific class is needed. All you need to do is add this line after you created your mock:
app()->instance(ExportApiService::class, $mock);
This way you tell Laravel that every time you need the ExportApiService
, Laravel should return the $mock
instead.
Upvotes: 1
Reputation: 62228
You created a mocked object, but you didn't bind it into the container. Without binding, when Laravel runs your command, it will just generate a new ExportApiService
instance.
public function setUp()
{
parent::setUp();
$mock = $this->createMock(ExportApiService::class);
$mock->method('exportData')->willReturn(123);
// bind the mock in the container, so whenever you ask for
// a new ExportApiService, you'll get your mocked object.
$this->app->instance(ExportApiService::class, $mock);
}
Another option you have, instead of doing this, is to mock your Guzzle requests. This way, all of your code executes as it normally would, but when you make your API call, Guzzle returns a pre-determined response instead of actually making the call. You can find the Guzzle docs on testing here.
After you configure your client with the mocked responses, you would bind that client instance into the container, so that when your ExportApiService
is created, Laravel injects the client you've setup with the mocked responses.
Upvotes: 4