Laravel Feature Testing: Call an external HTTP Request only once - laravel

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"...'

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

Mocking Dependency Injected URL parameter in PHPUnit

I am trying to create a test for a feature I've written.
The logic is quite simple:
From the api.php I am calling the store method:
Route::group(['prefix' => '/study/{study}/bookmark_list'], function () {
...
Route::post('/{bookmarkList}/bookmark', 'BookmarkController#store');
...
});
thus I am injecting the study and the bookmark list.
My controller passes down the parameters
public function store(Study $study, BookmarkList $bookmarkList)
{
return $this->serve(CreateBookmarkFeature::class);
}
And I am using them in the Feature accordingly
'bookmark_list_id' => $request->bookmarkList->id,
class CreateBookmarkFeature extends Feature
{
public function handle(CreateBookmarkRequest $request)
{
//Call the appropriate job
$bookmark = $this->run(CreateBookmarkJob::class, [
'bookmark_list_id' => $request->bookmarkList->id,
'item_id' => $request->input('item_id'),
'type' => $request->input('type'),
'latest_update' => $request->input('latest_update'),
'notes' => $request->input('notes')
]);
//Return
return $this->run(RespondWithJsonJob::class, [
'data' => [
'bookmark' => $bookmark
]
]);
}
}
I am also using a custom request (CreateBookmarkRequest) which practically verifies if the user is authorised and imposes some rules on the input.
class CreateBookmarkRequest extends JsonRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return $this->getAuthorizedUser()->canAccessStudy($this->study->id);
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
"item_id" => ["integer", "required"],
"type" => [Rule::in(BookmarkType::getValues()), "required"],
"latest_update" => ['date_format:Y-m-d H:i:s', 'nullable'],
"text" => ["string", "nullable"]
];
}
}
Now, here comes the problem. I want to write a test for the feature that tests that the correct response is being returned (it would be good to verify the CreateBookmarkJob is called but not that important). The problem is that although I can mock the request, along with the input() method, I cannot mock the injected bookmarkList.
The rest of the functions are mocked properly and work as expected.
My test:
class CreateBookmarkFeatureTest extends TestCase
{
use WithoutMiddleware;
use DatabaseMigrations;
public function setUp(): void
{
parent::setUp();
// seed the database
$this->seed();
}
public function test_createbookmarkfeature()
{
//GIVEN
$mockRequest = $this->mock(CreateBookmarkRequest::class);
$mockRequest->shouldReceive('authorize')->once()->andReturnTrue();
$mockRequest->shouldReceive('rules')->once()->andReturnTrue();
$mockRequest->shouldReceive('input')->once()->with('item_id')->andReturn(1);
$mockRequest->shouldReceive('input')->once()->with('type')->andReturn("ADVOCATE");
$mockRequest->shouldReceive('input')->once()->with('latest_update')->andReturn(Carbon::now());
$mockRequest->shouldReceive('input')->once()->with('notes')->andReturn("acs");
$mockRequest->shouldReceive('bookmark_list->id')->once()->andReturn(1);
//WHEN
$response = $this->postJson('/api/recruitment_toolkit/study/1/bookmark_list/1/bookmark', [
"type"=> "ADVOCATE",
"item_id"=> "12",
"text"=> "My first bookmark"
]);
//THEN
$this->assertEquals("foo", $response['data'], "das");
}
One potential solution that I though would be to not mock the request, but this way I cannot find a way to mock the "returnAuthorisedUser" in the request.
Any ideas on how to mock the injected model would be appreciated, or otherwise any idea on how to properly test the feature in case I am approaching it wrong.
It is worth mentioning that I have separate unit tests for each of the jobs (CreateBookmarkJob and RespondWithJSONJob).
Thanks in advance
A feature test, by definition, will be imitating an end-user action. There's no need to mock the request class, you just make the request as a user would.
Assuming a Study with ID 1 and a BookmarkList with ID 1 have been created by your seeder, Laravel will inject appropriate dependencies via route model binding. If not, you should use a factory method to create models and then substitute the appropriate ID in the URL.
<?php
namespace Tests\Feature;
use Tests\TestCase;
class CreateBookmarkFeatureTest extends TestCase
{
use WithoutMiddleware;
use DatabaseMigrations;
public function setUp(): void
{
parent::setUp();
$this->seed();
}
public function TestCreateBookmarkFeature()
{
$url = '/api/recruitment_toolkit/study/1/bookmark_list/1/bookmark';
$data = [
"type"=> "ADVOCATE",
"item_id"=> "12",
"text"=> "My first bookmark"
];
$this->postJson($url, $data)
->assertStatus(200)
->assertJsonPath("some.path", "some expected value");
}
}
I agree with #miken32's response - that a feature should indeed imitate a user interaction - however the dependency injection via route model binding still did not work.
After spending some hours on it, I realised that the reason for it is that
use WithoutMiddleware;
disables all middleware, even the one responsible for route model binding, therefore the object models were not injected in the request.
The actual solution for this is that (for laravel >=7) we can define the middleware we want to disable, in this case:
$this->withoutMiddleware(\App\Http\Middleware\Authenticate::class);
Then we just use
$user = User::where('id',1)->first(); $this->actingAs($user);
And everything else works as expected.
DISCLAIMER: I am not implying that miken32's response was incorrect; it was definitely in the right direction - just adding this as a small detail.

Mocking class instantiation in a feature test

I'm currently working on my first Laravel project — a service endpoint that returns a resource based on a recording saved in S3. The service doesn't require a DB, but my idea was, I could keep the controller skinny, by moving the logic to a "model". I could then access the resource by mimic'ing some standard active record calls.
Functionally, the implementation works as expected, but I am having issues with mocking.
I am using a library to create signed CloudFront URLs, but it is accessed as a static method. When I first started writing my feature test, I found that I was unable to stub the static method. I tried class aliasing with Mockery, but with no luck — I was still hitting the static method. So, I tried wrapping the static method in a little class assuming mocking the class would be easier. Unfortunately, I'm experiencing the same issue. The thing that I am trying to mock is being hit as if I'm not mocking it.
This stack overflow post gives an example of how to use class aliasing, but I can't get it to work.
What is the difference between overload and alias in Mockery?
What am I doing wrong? I'd prefer to get mockery aliasing to work, but instance mocking would be fine. Please point me in the right direction.
 Thank you in advance for your help.
Controller
// app/Http/Controllers/API/V1/RecordingController.php
class RecordingController extends Controller {
public function show($id){
return json_encode(Recording::findOrFail($id));
}
}
Model
// app/Models/Recording.php
namespace App\Models;
use Mockery;
use Carbon\Carbon;
use CloudFrontUrlSigner;
use Storage;
use Illuminate\Support\Arr;
class Recording
{
public $id;
public $url;
private function __construct($array)
{
$this->id = $array['id'];
$this->url = $this->signedURL($array['filename']);
}
// imitates the behavior of the findOrFail function
public static function findOrFail($id): Recording
{
$filename = self::filenameFromId($id);
if (!Storage::disk('s3')->exists($filename)) {
abort(404, "Recording not found with id $id");
}
$array = [
'id' => $id,
'filename' => $filename,
];
return new self($array);
}
// imitate the behavior of the find function
public static function find($id): ?Recording
{
$filename = self::filenameFromId($id);
if (!Storage::disk('s3')->exists($filename)){
return null;
}
$array = [
'id' => $id,
'filename' => $filename,
];
return new self($array);
}
protected function signedURL($key) : string
{
$url = Storage::url($key);
$signedUrl = new cloudFrontSignedURL($url);
return $signedUrl->getUrl($url);
}
}
/**
* wrapper for static method for testing purposes
*/
class cloudFrontSignedURL {
protected $url;
public function __construct($url) {
$this->url = CloudFrontUrlSigner::sign($url);
}
public function getUrl($url) {
return $this->url;
}
}
Test
// tests/Feature/RecordingsTest.php
namespace Tests\Feature;
use Mockery;
use Faker;
use Tests\TestCase;
use Illuminate\Http\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Foundation\Testing\WithFaker;
/* The following is what my test looked like when I wrapped CloudFrontUrlSigner
* in a class and attempted to mock the class
*/
class RecordingsTest extends TestCase
{
/** #test */
public function if_a_recording_exists_with_provided_id_it_will_return_a_URL()
{
$recordingMock = \Mockery::mock(Recording::class);
$faker = Faker\Factory::create();
$id = $faker->numberBetween($min = 1000, $max = 9999);
$filename = "$id.mp3";
$path = '/api/v1/recordings/';
$returnValue = 'abc.1234.com';
$urlMock
->shouldReceive('getURL')
->once()
->andReturn($returnValue);
$this->app->instance(Recording::class, $urlMock);
Storage::fake('s3');
Storage::disk('s3')->put($filename, 'this is an mp3');
Storage::disk('s3')->exists($filename);
$response = $this->call('GET', "$path$id");
$response->assertStatus(200);
}
}
// The following is what my test looked like when I was trying to alias CloudFrontUrlSigner
{
/** #test */
public function if_a_recording_exists_with_provided_id_it_will_return_a_URL1()
{
$urlMock = \Mockery::mock('alias:Dreamonkey\cloudFrontSignedURL');
$faker = Faker\Factory::create();
$id = $faker->numberBetween($min = 1000, $max = 9999);
$filename = "$id.mp3";
$path = '/api/v1/recordings/';
$returnValue = 'abc.1234.com';
$urlMock
->shouldReceive('sign')
->once()
->andReturn($returnValue);
$this->app->instance('Dreamonkey\cloudFrontSignedURL', $urlMock);
Storage::fake('s3');
Storage::disk('s3')->put($filename, 'this is an mp3');
Storage::disk('s3')->exists($filename);
$response = $this->call('GET', "$path$id");
$response->assertStatus(200);
}
}
phpunit
$ phpunit tests/Feature/RecordingsTest.php --verbose
...
There was 1 failure:
1) Tests\Feature\RecordingsTest::if_a_recording_exists_with_provided_id_it_will_return_a_URL
Expected status code 200 but received 500.
Failed asserting that false is true.
/Users/stevereilly/Projects/media-service/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:133
/Users/stevereilly/Projects/media-service/tests/Feature/RecordingsTest.php:85
/Users/stevereilly/.composer/vendor/phpunit/phpunit/src/TextUI/Command.php:206
/Users/stevereilly/.composer/vendor/phpunit/phpunit/src/TextUI/Command.php:162
You're getting a 500, which means there's something wrong with the code. Just by scanning it I notice you're missing a filenameFromId method on the Recordings class, and the Test is creating a mock named $recordingMock, but you try to use $urlMock. Try to fix those issues first.
Then you're mocking the class, but you never replace it in your application (you did it in the old test apparently).
Generally you want to follow these steps when mocking:
1. Mock a class
2. Tell Laravel to replace the class with your mock whenever someone requests it
3. Make some assertions against the mock

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

Handling Custom Exceptions from PHPUnit (Laravel 5.2)

I'm currently playing around with Exception Handler, and creating my own custom exceptions.
I've been using PHPUnit to run tests on my Controller Resource, but when I throw my custom exceptions, Laravel thinks it's coming from a regular HTTP request rathen than AJAX.
Exceptions return different response based on wether it's an AJAX request or not, like the following:
<?php namespace Actuame\Exceptions\Castings;
use Illuminate\Http\Request;
use Exception;
use Actuame\Exceptions\ExceptionTrait;
class Already_Applied extends Exception
{
use ExceptionTrait;
var $redirect = '/castings';
var $message = 'castings.errors.already_applied';
}
And the ExceptionTrait goes as follows:
<?php
namespace Actuame\Exceptions;
trait ExceptionTrait
{
public function response(Request $request)
{
$type = $request->ajax() ? 'ajax' : 'redirect';
return $this->$type($request);
}
private function ajax(Request $request)
{
return response()->json(array('message' => $this->message), 404);
}
private function redirect(Request $request)
{
return redirect($this->redirect)->with('error', $this->message);
}
}
Finally, my test goes like this (excerpt of the test that's failing)
public function testApplyToCasting()
{
$faker = Factory::create();
$user = factory(User::class)->create();
$this->be($user);
$casting = factory(Casting::class)->create();
$this->json('post', '/castings/apply/' . $casting->id, array('message' => $faker->text(200)))
->seeJsonStructure(array('message'));
}
My logic is like this although I don't think the error is coming from here
public function apply(Request $request, User $user)
{
if($this->hasApplicant($user))
throw new Already_Applied;
$this->get()->applicants()->attach($user, array('message' => $request->message));
event(new User_Applied_To_Casting($this->get(), $user));
return $this;
}
When running PHPUnit, the error I get returned is
1) CastingsTest::testApplyToCasting PHPUnit_Framework_Exception:
Argument #2 (No Value) of PHPUnit_Framework_Assert:
:assertArrayHasKey() must be a array or ArrayAccess
/home/vagrant/Code/actuame2/vendor/laravel/framework/src/Illuminate/Foundation/T
esting/Concerns/MakesHttpRequests.php:304
/home/vagrant/Code/actuame2/tests/CastingsTest.php:105
And my laravel.log is over here http://pastebin.com/ZuaRaxkL (Too large to paste)
I have actually discovered that PHPUnit is not actually sending an AJAX response, because my ExceptionTrait actually changes the response on this. When running the test it takes the request as a regular POST request, and runs the redirect() response rather than ajax(), hence it's not returning the correspond.
Thanks a bunch!
I have finally found the solution!
As I said, response wasn't the right one as it was trying to redirect rathen than return a valid JSON response.
And after going through the Request code, I found out that I need to use also wantsJson(), as ajax() may not be the case always, so I have modified my trait to this:
<?php
namespace Actuame\Exceptions;
trait ExceptionTrait
{
public function response(Request $request)
{
// Below here, I added $request->wantsJson()
$type = $request->ajax() || $request->wantsJson() ? 'ajax' : 'redirect';
return $this->$type($request);
}
private function ajax(Request $request)
{
return response()->json(array('message' => $this->message), 404);
}
private function redirect(Request $request)
{
return redirect($this->redirect)->with('error', $this->message);
}
}

Resources