How to mock other class functions in testing controller in Laravel - 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...
}

Related

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

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

Laravel Eloquent: API Resources - How does it work, so that I can create a similar class?

Look at this example:
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/user/{id}', function ($id) {
return new UserResource(User::findOrFail($id));
});
How does this internally work? Because at first glance we just return an Object of the class UserResource. But Laravel is magically calling the toArray function and resolves it correctly.
I want the same for my Recommendation class to work.
use App\Http\Recommendations\RecentlyUpdatedRecommendation;
use ...
Route::get('/someurl', function () {
return ['collections' => [
new RecentlyUpdatedRecommendation(),
new WatchlistRecommendation(),
new LastWatchedRecommendation(),
...
]];
});
With
/**
* All Recommendations use this as output
*/
abstract class Recommendation
{
// No Idea what needs to be put here
}
The output array/json should resolve into this:
return [
'title' => $title,
'description' => $description,
'courses' => (collection) $courses
];
Any resource that you create is instance of Illuminate\Contracts\Support\Responsable Interface,
This means that each of them must have toResponse method implemented.
If we look at JsonResponse's toResponse method we see this code:
public function toResponse($request)
{
return (new ResourceResponse($this))->toResponse($request);
}
Internally it is calling Resource's resolve method.
resolve method is where we see toArray method to be called
public function resolve($request = null)
{
$data = $this->toArray(
$request = $request ?: Container::getInstance()->make('request')
);
}
By creating new response you are just overriding toArray method with your logic.
In Your case, you don't need to write too much to get toArray method "magically" called
use Illuminate\Contracts\Support\Arrayable;
use JsonSerializable;
abstract class Recommendation implements Arrayable, JsonSerializable {
}
...
class SomeRecomendation extends Recommendation {
public function jsonSerialize()
{
return $this->toArray();
}
public function toArray(){
return [
'title' => $title,
'description' => $description,
'courses' => (collection) $courses
];
}
}
Now watch will happen, When you return ['collecion' => new SomeRecomendation()], Laravel calls json_encode on it ( to return response as json ),
Because new SomeRecomendation() is instance of JsonSerializable it will call jsonSerialize method and encodes an array returned from it.
When you return only return new SomeRecomendation(); from controller, it will call, because it is instance of Arrayable, Laravel calls toArray method magically.
Hope this answer helps you.

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);

Laravel policy not working

In my Laravel application, i have several policies working, but one will not work.
Controller
public function store(Project $project, CreateActionRequest $request)
{
$this->authorize('store', $project);
Action::create([
'name' => $request->name,
]);
return redirect()->route('projects.show', $project->id)->withSuccess('Massnahme erfolgreich gespeichert');
}
Policy
namespace App\Policies\Project;
use App\Models\Project\Project;
use App\Models\User;
use App\Models\Project\Action;
use Illuminate\Auth\Access\HandlesAuthorization;
class ActionPolicy
{
use HandlesAuthorization;
public function store(User $user, Project $project)
{
return $user->company_id === $project->company_id;
}
}
AuthServiceProvider
protected $policies = [
'App\Models\User' => 'App\Policies\CompanyAdmin\UserPolicy',
'App\Models\Company' => 'App\Policies\CompanyAdmin\CompanyPolicy',
'App\Models\Team' => 'App\Policies\CompanyAdmin\TeamPolicy',
'App\Models\Department' => 'App\Policies\CompanyAdmin\DepartmentPolicy',
'App\Models\Location' => 'App\Policies\CompanyAdmin\LocationPolicy',
'App\Models\Division' => 'App\Policies\CompanyAdmin\DivisionPolicy',
'App\Models\Costcenter' => 'App\Policies\CompanyAdmin\CostcenterPolicy',
'App\Models\Workplace' => 'App\Policies\CompanyAdmin\WorkplacePolicy',
'App\Models\Product' => 'App\Policies\CompanyAdmin\ProductPolicy',
'App\Models\Project\Action' => 'App\Policies\Project\ActionPolicy',
'App\Models\Project\Project' => 'App\Policies\Project\ProjectPolicy',
];
CreateActionRequest
namespace App\Http\Requests\Project;
use Illuminate\Foundation\Http\FormRequest;
class CreateActionRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => 'required|min:3',
];
}
}
All policies are working except ActionPolicy and ProjectPolicy.
I added in the policy a __construct() method to check if the policy is called. But ActionPolicy and ProjectPolicy are not working.
How can i search the error? I tried with dd() but i got only allways the message: This action is unauthorized
Since you are injecting CreateActionRequest instead of Request that means you are defining your own set of rules to authorize the FormRequest which comes inside of your method. Further it means that you gotta define a few rules which the "FormRequest" has to pass in order to EVEN reach your controller, this is a nice concept that I like about Laravel since the code is not centralized, but rather spread and every layer has it's own responsibility. Now, you don't have to call any method from your CreateActionRequest nor you have to write any code regarding that class in your controller, because Laravel runs authorize method by default before allowing the Request to reach your controller, before running authorizemethod in your CreateActionRequest it runs rules method which verifies that all the given fields pass the expressions you assigned them, so the execution is something like this CreateActionRequest => rules => authorize => IF(authorized) Controller ELSE Not authorized, hope that makes sense. In order to fix your code:
1.) Remove $this->authorize('store', $project);
This will allow you to pass not authorized error in case your name passes the truth test inside of rules method inside of your CreateActionRequest. If you wish to utilize your Action Policy you will need to hook up your custom Request(CreateActionRequest) with it and this is how:
public function authorize()
{
$store = $this->route('project');
//The above line will return Project object if your mapping is correct
//If it's not it will return the value you passed to your route for {project}
return $this->user() && $this->user()->can('store', $store);
}
EDIT:
Here is the link where you can see how to properly authorize and connect policy with CreateActionRequest
Do you have all your controller methods defined with the Request object last?
public function store(Project $project, CreateActionRequest $request)
The Request object should be the first parameter in the methods signature:
public function store(CreateActionRequest $request, Project $project)
Dependency Injection & Route Parameters
If your controller method is also expecting input from a route parameter you should list your route parameters after your other dependencies.
Most Laravel authorization mechanisms have identical method signatures allowing them to work across varying classes.

Resources