How can I mock a service class inside a Laravel Job? - laravel

I want to mock a service who call a third party, but I can't figure it out.
I have a controller method who has a service injected on it and do stuff:
public function store(Request $request, MyService $myService)
{
$data = $request->validated();
$myService->create($data, $request->user());
return response()->json();
}
In this service, I call a job to do other stuffs:
MyJob::dispatch($manager);
My job is built like this:
public function __construct(private Manager $manager)
{
}
public function handle()
{
// THE SERVICE I WANT TO MOCK
$this->managementService = resolve(ManagementService::class, ['manager_id' => $this->manager->id]);
$this->doStuff();
}
private function doStuff() {
$this->managementService->startManagement();
}
In ManagementService I want to mock the function callApi:
public function startManagement()
{
$data = $this->callApi('/thirdparty/call');
return $data;
}
SO, in my test I try to mock the ManagementService and call my route who do all these things
$this->mock(ManagementService::class, function ($mock) {
$mock->shouldReceive('callApi')->andReturn('none');
});
$response = $this->actingAs(User::factory()->create())->post('/myroute', [
'manager_id' => 4,
]);
But it seems this mock is never used, it's still going into the "normal" Management Service, because when I dump $data in the startManagement method when I launch tests, it's not returning 'none'.
What am I doing wrong?
Thanks

The code that you post is not very clear but if i understand correctly you like to mock a hard dependency thats why your mock never called because you never register it.
Just add the string overload before the class name in the mock method.

Figured it out by injecting my service directly in the job
MyJob::dispatch($manager, resolve(ManagementService::class, ['manager_id' => $this->manager->id]));
And inside the service instance it via constructor instead of in the handle
public function __construct(public Manager $manager, ManagementService $managementService)
{
$this->managementService = $managementService;
}
Then in my test, my mock is instancied like this:
$mock = Mockery::mock(ManagementService::class);
$mock->shouldReceive('callApi')
->once()
->andReturn('none');
Thanks to all

Related

Laravel Feature Testing: Call an external HTTP Request only once

I have a login function that needs to be called from a separate User Service API.
The sole purpose of logging in is to be used on testing, because I need to get the bearer token that will be used as the parameter for one of my middleware.
As for the testing, is it possible to call external api thru HTTP Request only once? If so, where should I put it?
I tried it on the setUp() function but it seems to be called every time a test function is executed on the test class, making the test slow.
EDITED with Code:
The test code:
<?php
namespace Tests\Feature\Controllers;
use Illuminate\Foundation\Testing\RefreshDatabase;
use PHPUnit\Framework\TestSuite;
use Tests\TestCase;
class MyTest extends TestCase
{
protected string $bearerToken;
public function setUp(): void
{
parent::setUp();
$this->bearerToken = self::getToken();
}
protected static function getToken()
{
$response = Http::post('http://auth_api/oauth/token', [
...
...
]);
// but assume that this request always succeed.
if ($response->failed()) return [];
return json_decode(json_encode($response->json()), true)['access_token'];
}
...test methods here
}
I also tried doing manual flagging, so that the custom login function will only be fetched once throughout the whole test suite.
like below:
protected static $isInitiated = false;
protected string $bearerToken;
public function setUp(): void
{
parent::setUp();
if (! self::$isInitiated) {
$this->bearerToken = self::getToken();
self::$isInitiated = true;
}
}
Based on the answer here
but it gives me error saying:
$bearerToken must not be accessed before initialization
So, from that error, the test methods must've been executed first before it even gave value to $bearerToken.
I also tried public static function setUpBeforeClass():
protected static ?string $bearerToken = null;
public static function setUpBeforeClass(): void
{
self::$bearerToken = self::getToken();
}
But it also gives me error saying:
A facade root has not been set.
Is there any way to do this?
You shouldn't call any external api in testing, the reason why is simple, You are testing the app you or your team coded, not someone else.
So, you should mocking all external parts like use Http::fake().
Http::fake([
// Stub a JSON response for GitHub endpoints...
'github.com/*' => Http::response(['foo' => 'bar'], 200, $headers),
// Stub a string response for Google endpoints...
'google.com/*' => Http::response('Hello World', 200, $headers),
]);
Here is document: https://laravel.com/docs/9.x/http-client#faking-responses
And if you really want to call external api, new a GuzzleClient, then you can do what you want, but you should realize that is not a good idea.
$client = new GuzzleHttp\Client();
$res = $client->request('GET', 'https://api.github.com/user', [
'auth' => ['user', 'pass']
]);
echo $res->getStatusCode();
// "200"
echo $res->getHeader('content-type')[0];
// 'application/json; charset=utf8'
echo $res->getBody();
// {"type":"User"...'

How to mock other class functions in testing controller in Laravel

I am trying to test a controller in Laravel that uses another class as a helper which calls an API and returns the result.
To avoid external API calls, I need to mock this helper.
I tried to mock the class inside the controller and run the test but I didn't get what I expected in mock class.
this is my controller method:
public function A(Request $request){
$helper = new TheHelper();
$result = $helper->getResult($request->email);
if($result){
return response()->json([
'success' => true,
'message' => "result found",
], 200);
}else{
return response()->json([
'success' => false,
'message' => "no result",
], 500);
}
}
My helper method simply calls an API and returns the result.
class TheHelper
{
public function getResult($email){
// some api calls
return $result;
}
}
Here is my test:
public function testExample()
{
$helperMock = Mockery::mock(TheHelper::class);
// Set expectations
$helperMock ->shouldReceive('getResult')
->once()
->with('testemail#test.com')
->andReturn([
'id' => '100'
]);
$this->app->instance(TheHelper::class, $helperMock);
$this->json(
'POST',
'/api/test_method',
['email' => 'testemail#test.com'])
->assertStatus(200);
}
My mock function never called. it only checks with the real API inside TheHelper method
Your test is creating a mock object and binding that mock object into the Laravel service container. However, your controller is not pulling a TheHelper instance from the Laravel service container; it is manually instantiating it with the new keyword. Using the new keyword is core PHP, and does not involve Laravel at all.
Your test is showing you an issue in your code. TheHelper is a dependency of your method, and should therefore be passed into the method instead of being created inside the method.
You either need to update your controller method to use dependency injection, so that Laravel can automatically resolve the TheHelper dependency from its container, or you need to replace your new keyword with a call into the Laravel container.
Using dependency injection:
public function A(Request $request, TheHelper $helper)
{
$result = $helper->getResult($request->email);
// rest of function...
}
Manually pull from the container:
public function A(Request $request)
{
$helper = app(TheHelper::class);
$result = $helper->getResult($request->email);
// rest of function...
}

Why would a service provider not be mocked?

I'm using a library to send send requests to Indeed jobs https://github.com/jobapis/jobs-indeed.
I have setup a provider so I can easily mock the requests and also so I don't have to setup my credentials every time I use it.
This library has 2 classes. A Query and Provider class. The Provider class is responsible for making the http request.
I can mock the Query class but I can't mock the Provider class.
Provider:
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use JobApis\Jobs\Client\Queries\IndeedQuery;
use JobApis\Jobs\Client\Providers\IndeedProvider;
class JobSearchServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* #return void
*/
public function register()
{
// Register Indeeds API
$this->app->bind(IndeedQuery::class, function() {
// Build the required fields for indeeds api
$indeed = new IndeedQuery([
'publisher' => config('services.indeed.publisher'),
'format' => 'json',
'v' => '2',
]);
return $indeed;
});
$this->app->bind(IndeedProvider::class, function() {
// Use an empty query object so that we can initialise the provider and add the query in the controller.
$queryInstance = app('JobApis\Jobs\Client\Queries\IndeedQuery');
return new IndeedProvider($queryInstance);
});
}
}
Controller:
public function searchIndeed(Request $request, IndeedQuery $query, IndeedProvider $client)
{
dump($query); // Returns a mockery object
dd($client); // Returns original object
}
Test:
public function testSearchIndeed()
{
$user = factory(User::class)->create();
$this->mock(IndeedQuery::class);
$this->mock(IndeedProvider::class);
$this->actingAs($user)
->get('indeed')
->assertStatus(200);
}
Why is the IndeedQuery being mocked but not the IndeedProvider?
Found the problem.
Mockery doesn't throw errors if you try and mock a class that doesn't exist. I had a spelling mistake in my tests when requiring the class.
Controller
use JobApis\Jobs\Client\Providers\IndeedProvider;
Test
use JobApis\Jobs\Client\Provider\IndeedProvider; // Notice missing 's'
When using mockery you won't get errors if the class doesn't exist. So if the mockery object isn't being resolved check the spelling.

Laravel + Mockery InvalidCountException

I am trying to mock a class to prevent it from having to call 3rd party apis. But when setting up the mock, it doesn't seem to affect the controller action. I did try replacing the $this->postJson() by manually creating instances of the Request- and OEmbedController-classes. The create()-method is getting called, but I am receiving an error from Mockery that it isn't.
What am I doing wrong here?
Error:
Mockery\Exception\InvalidCountException : Method create() from Mockery_2_Embed_Embed should be called exactly 1 times but called 0 times.
Test:
class OEmbedTest extends TestCase
{
public function tearDown()
{
Mockery::close();
}
/**
* It can return an OEmbed object
* #test
*/
public function it_can_return_an_o_embed_object()
{
$url = 'https://www.youtube.com/watch?v=9hUIxyE2Ns8';
Mockery::mock(Embed::class)
->shouldReceive('create')
->with($url)
->once();
$response = $this->postJson(route('oembed', ['url' => $url]));
$response->assertSuccessful();
}
}
Controller:
public function __invoke(Request $request)
{
$info = Embed::create($request->url);
$providers = $info->getProviders();
$oembed = $providers['oembed'];
return response()
->json($oembed
->getBag()
->getAll());
}
It seems you are mocking the Embed class the wrong way. If you use the Laravel facade method shouldReceive() instead of creating a Mock of the class itself, the framework will place the mock in the service container for you:
Embed::shouldReceive('create')
->with($url)
->once();
instead of
Mockery::mock(Embed::class)
->shouldReceive('create')
->with($url)
->once();
Also be aware that if the parameters your tested code passes to the mock differs from what you learned the mock with with($url), the mock considers itself uncalled. But you'll receive another error for calling a not defined method anyway.
I was able to solve this by using this in my test:
protected function setUp()
{
parent::setUp();
app()->instance(Embed::class, new FakeEmbed);
}
Then resolving it like this
$embed = resolve(Embed::class);
$embed = $embed->create($url);

Access Request in Service Provider After Applying Middleware

Bindings
I'm using bindings in my service provider between interface and implementation:
public function register()
{
$this->app->bind('MyInterface', MyImplementation::class);
}
Middleware
In my middleware, I add an attribute to the request:
public function handle($request, Closure $next)
{
$request->attributes->add(['foo' => 'bar]);
return $next($request);
}
Now, I want to access foo in my service provider
public function register()
{
$this->app->bind('MyInterface', new MyImplementation($this->request->attributes->get('foo')); // Request is not available
}
The register() is called before applying the middleware. I know.
I'm looking for a technique to 'rebind' if the request->attributes->get('foo') is set
Try like this:
public function register()
{
$this->app->bind('MyInterface', function () {
$request = app(\Illuminate\Http\Request::class);
return app(MyImplementation::class, [$request->foo]);
}
}
Binding elements works like this that they will be triggered only when they are call.
In service provider You can also access Request Object by:
public function register()
{
$request = $this->app->request;
}
The accepted answer is good, however it does not address the issues regarding DI. So in your Service Provider you need:
public function register()
{
$this->app->bind('MyInterface', function () {
return new MyImplementation(request()->foo);
}
}
But you need to be careful with DI. If you do this in your Controller:
class MyController extends Controller
{
public function __construct(MyInterface $myInterface)
{
$this->myInterface = $myInterface;
}
}
It will NOT work! The constructor of the controller is called BEFORE the group middleware is applied, so the foo parameter will be null on MyImplementation.
If you want to use DI, you need to either resolve it using App::make(MyInterface::class) outside of the constructor, or even better pass your dependency in the Controller's method:
class MyController extends Controller
{
public function index(MyInterface $myInterface)
{
$myInterface->getFoo();
}
}
Above will work because the controller's method is executed after the middlewares are applied.
This is the flow of a laravel request:
Global middleware run
Target controller's __construct run
Group middleware run
Target controller's method/action run (in above case index)
Try this
public function register()
{
$this->app->bind('MyInterface', function ($app) {
return new MyImplementation(request()->foo);
}
}

Resources