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
Related
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"...'
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);
I'm testing a class that calls a custom service and want to mock out the custom service.
The error is:
App\Jobs\CustomApiTest::getrandomInfo
Error: Call to a member function toArray() on null
This is because in getrandomInfo() there is a database call to fetch an ID and the test database is currently returning null because there is no entry, but the test should never even go that far because I am mocking out the getData function.
Machine Config:
Laravel 5.2
PHPUnit 4.8
I can not update my configuration.
MainClass.php
namespace App\Jobs;
use App\Services\CustomApi;
class MainClass
{
public function handle()
{
try {
$date = Carbon::yesterday();
$data = (new CustomApi($date))->getData();
} catch (Exception $e) {
Log::error("Error, {$e->getMessage()}");
}
}
}
MainClassTest.php
nameSpace App\Jobs;
use App\Services\CustomApi;
class MainClassTest extends \TestCase
{
/** #test */
public function handleGetsData()
{
$data = json_encode([
'randomInfo' => '',
'moreInfo' => ''
]);
$customApiMock = $this->getMockBuilder(App\Services\CustomApi::class)
->disableOriginalConstructor()
->setMethods(['getData'])
->getMock('CustomApi', ['getData']);
$customApiMock->expects($this->once())
->method('getData')
->will($this->returnValue($data));
$this->app->instance(App\Services\CustomApi::class, $customApiMock);
(new MainClass())->handle();
}
}
CustomApi Snippet
namespace App\Services;
class CustomApi
{
/**
* #var Carbon
*/
private $date;
public function __construct(Carbon $date)
{
$this->date = $date;
}
public function getData() : string
{
return json_encode([
'randomInfo' => $this->getrandomInfo(),
'moreInfo' => $this->getmoreInfo()
]);
}
}
I have tried many variations of the above code including:
Not using `disableOriginalConstructor()` when creating $externalApiMock.
Not providing parameters to `getMock()` when creating $externalApiMock.
Using `bind(App\Services\CustomApi::class, $customApiMock)` instead of instance(App\Services\CustomApi::class, $customApiMock) for the app.
Using willReturn($data)`` instead `will($this->returnValue($data))`.
I ended up creating a Service Provider and registering it in the app.php file. It seemed like the application was not saving the instance in the containers but works when it is bound to the service.
Hello I have a test case which will call a route and it will return some data if the session will set.
Here is my test case
class TestControllerTest extends TestCase
{
// ...
public function testResponseOfJson()
{
$response = $this->call('GET', 'profile/test');
$this->assertEmpty( !$response );
}
// ...
}
and here is my controller
Class TestController{
public function sendResponse(Request $request){
$this->data['user_id'] = $this->request->session()->get('userdata.userid');
if($this->data['userid']){
return data;
}
else{
return Failed;
}
}
}
My routes.php
Route::get('profile/test',['uses'=>'TestController#sendResponse']);
how can i set the session variable userdata.userid and get while doing unit testing.
Please check this page.
Laravel gives you the capability of using withSession chain method and other functions that will help you test where a session is required or needs to be manipulated in some way.
Example:
<?php
class ExampleTest extends TestCase
{
public function testApplication()
{
$response = $this->withSession(['foo' => 'bar'])
->get('/');
}
}
There is a need to perform a specific process multiple threads. I learned about the extension for php - pthreads.
For example, a simple script outside Laravel works fine and I liked the results. I decided to move in Laravel, and faced with the problem. Of course I searched in google, found some questions on stackoverflow, where replied the author of extension. But me did not help his answers, so I ask you to help me.
Answered Question extension author.
There is a class App\Commands\QuestionsParserCommand. Inside I created an instance of the class App\My\Questions\QuestionsParser and call the method init(). Then the code of the method init():
// Create a pool
$pool = new Pool($this->threads, ParserWorkers::class);
// Create a thread class
$thread = new class extends Threaded
{
public function run()
{
// The class will receive data from a provider
// that will be shared between threads through ParserWorkers.
// It will work with the API and store the data in the database.
// The need to work with the threads,
// because the data for processing an incredible amount.
echo '+';
}
};
// Start a threads
for ($i = 0; $i < $this->threads; $i++) {
$pool->submit($thread);
}
$pool->shutdown();
Class ParserWorkers inherits from Worker and yet has an empty method run().
As a result, I run the script and get a message in the log of php:
[13-Oct-2016 11:27:35 Europe/Moscow] PHP Fatal error: Uncaught Exception: Serialization of 'Closure' is not allowed in [no active file]:0
Stack trace:
#0 {main}
thrown in [no active file] on line 0
Information: Laravel 5.2.43, php 7.0.8, Windows
Thank you all!
First you can get acquainted with the strategy of the author of the library: https://github.com/krakjoe/pthreads-autoloading-composer
The strategy used here ensures that each thread (Worker) gets a thread local copy of the framework, or whatever is being loaded, but does not break the abilities of objects that descend from pthreads.
Secondly, to start working with laravel features (like Service Providers, Facades, Helpers, etc), you need to initialize laravel.
I looked at how to initialize the application in the tests/CreatesApplication.php file. The code below shows how to do this for Laravel 5.7 with php 7.2.
Important: specify the correct path to autoload.php and app.php relative to the Autoloader.php file location.
namespace App\Console\Commands;
use Worker;
use Illuminate\Contracts\Console\Kernel;
class Autoloader extends Worker
{
public function run()
{
require __DIR__. '/../../../vendor/autoload.php';
$app = require __DIR__.'/../../../bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
}
public function start($options = PTHREADS_INHERIT_ALL)
{
return parent::start(PTHREADS_INHERIT_INI);
}
}
namespace App\Console\Commands;
use Exception;
use Threaded;
class OperatorThread extends Threaded
{
/**
* #var OperatorThreaded
*/
private $operator;
private $error;
public function __construct(OperatorThreaded $operator)
{
$this->operator = $operator;
}
public function run()
{
try {
$this->operator->handle();
} catch (Exception $exception) {
$this->error = (string) $exception;
}
}
public function getError() {
return $this->error;
}
}
namespace App\Console\Commands;
class OperatorThreaded
{
private $i;
public function __construct($i)
{
$this->i = $i;
}
public function handle()
{
sleep(rand(1,3));
if (app()->isBooted()) {
echo $this->i . PHP_EOL;
}
}
}
Now you can use Laravel with pthreads:
namespace App\Console\Commands;
use Pool;
use Illuminate\Console\Command;
class Test extends Command
{
protected $description = 'test';
protected $signature = 'test';
public function handle()
{
$operators = [];
for ($i=0; $i<5; $i++) {
$operators[] = new OperatorThreaded($i);
}
$pool = new Pool(count($operators), Autoloader::class);
foreach ($operators as $operator) {
$thread = new OperatorThread($operator);
$pool->submit($thread);
}
while ($pool->collect());
$pool->shutdown();
}
}
$ php artisan test
1
2
4
0
3
foreach ($this->pool as $w) {
$w->start(PTHREADS_INHERIT_ALL ^ PTHREADS_INHERIT_CLASSES);
}