How To Test Artisan Commands with ask() in Laravel 5.4 - laravel

Trying to write a test for laravel php artisan command with ask() function. I have never used mockery before, but when i try to run test, it freezes, so i guess, i'm doing something wrong.
MyCommand.php:
public function handle()
{
$input['answer1'] = $this->ask('Ask question 1');
$input['answer2'] = $this->ask('Ask question 2');
$input['answer3'] = $this->ask('Ask question 3');
//--- processing validation
$validator = Validator::make($input, [
'answer1' => 'required',
'answer2' => 'required',
'answer3' => 'required',
]);
if ($validator->fails()) {
// processing error
}
} else {
// saving to DB
}
}
And my unit test:
$command = m::mock('\App\Console\Commands\Questions');
$command->shouldReceive('ask')
->andReturn('Answer 1')
->shouldReceive('ask')
->andReturn('Answer 2')
->shouldReceive('ask')
->andReturn('Answer 3')
$this->artisan('myCommand:toRun');
$this->assertDatabaseHas('myTable', [
'question1' => 'answer1'
]);

Laravel 5.4 - 5.6
The actual issue here is that running the console command is waiting for user input, however we are running this through PHPUnit so we are unable to enter anything.
Bumping up against limitations in unit testing can initially be frustrating, however limitations you find can end up being a blessing in disguise.
Currently, your implementation is tightly coupled to a view (a console command, so a view to an admin, but still a view none-the-less.) What could be done here is place any logic within a separate class which MyCommand can utilize, and which PHPUnit can actually test on their own. We know that the fundamentals of running a custom command work, as demonstrated in Laravel unit tests, so we can offload our logic in a separate, testable class.
Your new class might look something like this:
class CommandLogic
{
public function getQuestion1Text()
{
return 'Ask question 1';
}
public function getQuestion2Text()
{
return 'Ask question 2';
}
public function getQuestion3Text()
{
return 'Ask question 3';
}
public function submit(array $input)
{
$validator = \Illuminate\Support\Facades\Validator::make($input, [
'answer1' => 'required',
'answer2' => 'required',
'answer3' => 'required',
]);
if ($validator->fails()) {
// processing error
} else {
// saving to DB
}
}
}
...your actual unit test, something like this:
$commandLogic = new CommandLogic();
$sampleInput = [
'answer1' => 'test1',
'answer2' => 'test2',
'answer3' => 'test3',
];
$commandLogic->submit($sampleInput);
$this->assertDatabaseHas('myTable', [
'question1' => 'test1'
]);
...and your console command, something like this:
public function handle()
{
$commandLogic = new CommandLogic();
$input['answer1'] = $this->ask($commandLogic->getQuestion1Text());
$input['answer2'] = $this->ask($commandLogic->getQuestion2Text());
$input['answer3'] = $this->ask($commandLogic->getQuestion3Text());
$commandLogic->submit($input);
}
This enforces the single responsibility principle and separates the moving pieces in your codebase. I know this may feel like a bit of a cop out, but testing this stuff in Laravel 5.4 is tough. If you are willing to upgrade to 5.7 or higher, read below...
Laravel 5.7+
Laravel 5.7 introduced being able to run console tests, which satisfies the exact requirement this question is asking - https://laravel.com/docs/5.7/console-tests. This is more of a full integration test rather than a unit test.

Related

PHP Unit Test Closure functions

I was performing unit test in my application, and some of the functions include closure functions like below:
<?php
namespace JobProgress\Transformers;
use League\Fractal\TransformerAbstract;
class CustomersTransformer extends TransformerAbstract {
public function includesCreatedBy($customer) {
$user = $customer->createdBy;
if($user) {
return $this->item($user, function($user){
\Log::info('unit test');
return [
'id' => (int)$user->id,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'full_name' => $user->full_name,
'full_name_mobile' => $user->full_name_mobile,
'company_name' => $user->company_name,
];
});
}
}
}
Note : I have extended TransformerAbstract class of Fractal and using the item function as $this->item from the TransformerAbstract class.
Here is how i am executing my test:
public function testIncludeDeletedBy() {
$customer = factory(Customer::class)->create();
$include = $this->transformer->includesCreatedBy($customer);
$object = $include->getData()->first();
$this->assertInstanceOf(User::class, $object);
}
My unit test is not executing the code that i have written in closure function.
Like as i have mentioned in above code, i have added some logs but my unit test is not executing that portion of the code.
Can anyone please help me
This might be related to the implementation detail of the $this->item() method. You may expect the closure to be executed while as we can see, the closure only needs to be parsed and then passed to the method as a parameter.
The code shows nothing that it is actually executed and therefore you should not expect this to be as it is an implementation detail not under your test.
You could de-anonymize the closure and call it directly to be able to test it as a unit.
So you have one test for the if-clause in the existing method and one test which allows you to run (not only parse) the code you currently have as an anonymous function / closure.
This is sometimes called a test-point and can bring you up speed. The smaller the units you have are, the easier it is to test them.
<?php
namespace JobProgress\Transformers;
use League\Fractal\TransformerAbstract;
class CustomersTransformer extends TransformerAbstract {
public function includesCreatedBy($customer) {
$user = $customer->createdBy;
if($user) {
return $this->item($user, $this->transformImplementation(...));
}
}
public function transformImplementation(object $user) {
\Log::info('unit test');
return [
'id' => (int)$user->id,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'full_name' => $user->full_name,
'full_name_mobile' => $user->full_name_mobile,
'company_name' => $user->company_name,
];
}
}
For the three dots (...), this is PHP 8.1 syntax, compare PHP RFC: First-class callable syntax for alternative PHP syntax before 8.1 for callables, e.g.:
Closure::fromCallable([$this, 'transformImplementation'])

Can't get user while testing auth in laravel

I'm writing automated tests for a legacy laravel project, 5.8.38.
I have this test method.
public function testUserReceivesAnEmailWithAPasswordResetLink()
{
Notification::fake();
$user = factory(User::class)->create([
'email' => 'john#example.com',
]);
$this->post($this->passwordEmailPostRoute(), [
'email' => 'john#example.com',
]);
$this->assertNull($token = DB::table('password_resets')->first());
Notification::assertSentTo($user, ResetPassword::class, function ($notification, $channels) use ($token) {
return Hash::check($notification->token, $token->token) === true;
});
}
This always fails because the user cannot be retrieved. The passwordEmailPostRoute() method goes to the src/Illuminate/Auth/Passwords/PasswordBroker.php sendResetLink() method, eventually ending up in src/Illuminate/Auth/EloquentUserProvider.php at retrieveByCredentials() method.
This always returns null.
I tried dumping data and queries, but everything failed. Any ideas appreciated.
This seems to be a very specific issue which I caused for myself.
My user factory generated wrong values for a morph connection field which prevented the return of a valid User object. I had to change the factory and the issue is now resolved.

How to test a controller with model injection but without middleware?

When I test this:
use WithoutMiddleware;
public function testPutSportOK()
{
$sport = Sport::first();
$sportName = 'Modification '.$sport->sport_name;
$position = random_int(0,100);
$post = [
'sport_name' => $sportName,
'position' => $position
];
$response = $this->json('PUT', '/api/sports/'.$sport->id, $post);
$response->assertStatus(200);
The test failed because I use the model injection in my controller. I understand that this injection needs the "bindings" middleware. But as I disabled all the middlewares, this injection cannot be done.
I disabled the middlewares for authentication reasons.
I tried to add this:
$this->withMiddleware('bindings');
But it's still the same.
How to test a controller using the model injection and without middlewares?
Edit
Add the controller with the model injection:
public function update(Request $request, Sport $sport)
{
// var_dump($sport);
$validator = Validator::make($request->all(), [
'sport_name' => 'required',
'position' => 'required|int'
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 400);
}
try {
// not necessary with the injection model
// $sport = Sport::findOrFail($id);
$sport->fill($request->all());
$sport->save();
return new SportResource($sport);
} catch (\Exception $ex) {
return response()->json($ex->getMessage(), 400);
}
}
From the source code here, you can actually disable "some" of your middlewares (instead of disabling them all) by giving an array of middlewares you WANT to disable.
$this->withoutMiddleware([
\App\Http\Middleware\Authenticate,
\App\Http\Middleware\RedirectIfAuthenticated,
// Add more here
]);
Finally I gave up the idea of ​​model injection. To prefer the passage of a classic id between the route and the controller.
As this my PHPUnit + Postman tests work perfectly and I'm not bored with this "binding" middleware any more.
Certainly it requires to write one more line in the controller to read the corresponding model in database. But it's only one line. I accept it !

Testing interactive artisan commands using Mockey

I am writing unit tests for interactive commands in Laravel 5.3 following this guide but I can't seem to get Mockery to work.
I've set up a simple command, greet:user.
public function handle()
{
if(!$name = $this->argument('name')) {
$name = $this->ask('Name of user to greet');
}
$this->info("Hello {$name}.");
}
And am writing unit-tests like;
public function testCanGreetGivenUser()
{
$command = Mockery::mock('App\Console\Commands\GreetUser[info]');
$command->shouldReceive('info')->once()->with('Hello Brian.');
$exit_code = Artisan::call('greet:user', ['name' => 'Brian', '--no-interaction' => true]);
$this->assertEquals(trim(Artisan::output()), 'Hello Brian.');
$this->assertEquals($exit_code, 0);
}
Issue:
Mockery\Exception\InvalidCountException: Method info("Hello Brian.") from Mockery_0_App_Console_Commands_GreetUser should be called
exactly 1 times but called 0 times.
My goal is to test;
If no input expected
$this->artisan('greet:user', ['name' => 'Brian'])
->expectsOutput('Hello Brian.')
->assertExitCode(0);
If input is required.
$this->artisan('greet:user')
->expectsQuestion('Name of user to greet', 'James')
->expectsOutput('Hello James.')
->assertExitCode(0);
That's how I could do it in Laravel 5.7, but how can I achieve the same for Laravel 5.3
To solve the problem you'll have to call the test as follows:
public function testCanGreetGivenUser()
{
$command = Mockery::mock('\App\Console\Commands\GreetUser[info]');
$command->shouldReceive('info')->once()->with('Hello Brian.');
$this->app[\Illuminate\Contracts\Console\Kernel::class]->registerCommand($command);
$exit_code = $this->artisan('greet:user', ['name' => 'Brian', '--no-interaction' => true]);
$this->assertEquals($exit_code, 0);
}
The deciding factor is the command registration. You must do it to actually replace the existing instance of the command with your mock.

Lumen job dispatching done without database Queue Driver

What do I have:
Lumen service which processing particular Job
Laravel portal which sending file to that service for processing by it
Once it was using only JS and Ajax it worked almost fine - the only what I had to implement is CORS middleware. However after I moved logic to JWT (using jwt-auth package) and GuzzleHttp (I'm using it to send requests to service API) Job stopped processing throught database queue instead it running as if Queue driver being set to sync.
Following is controller which I'm calling during API call:
public function processPackageById(Request $request) {
$id = $request->package_id;
$package = FilePackage::where('id', '=', $id)->where('package_status_id', '=', 1)->first();
if($package) {
Queue::push(new PackageProcessingJob(
$this->firm,
$this->accounts,
$package
));
return 'dispatching done for ' . $id;
}
return 'dispatching not done for ' . $id;
}
where $this->firm and $this->accounts are injected Repositories for particular models. FilePackage object being created on Laravel site and both shares same database to work with.
As result no job being incerted into jobs table. When I use Postman everything is fine. However when I'm trying to send request from Laravel backend:
public function uploaderPost(Request $request)
{
// Here we get auth token and put into protected valiable `$this->token`
$this->authorizeApi();
$requestData = $request->except('_token');
$package = $requestData['file'];
$uploadPackageRequest =
$this->client->request('POST', config('bulk_api.url') .'/api/bulk/upload?token=' . $this->token,
[
'multipart' => [
[
'name' => 'file',
'contents' => fopen($package->getPathName(), 'r'),
'filename' => $package->getClientOriginalName(),
],
]
]);
$uploadPackageRequestJson = json_decode($uploadPackageRequest->getBody()->getContents());
$uploadPackageRequestStatus = $uploadPackageRequestJson->status;
if($uploadPackageRequestStatus == 1) {
$package = BulkUploadPackage::where('id', '=',$uploadPackageRequestJson->id)->first();
// If package is okay - running it
if($package !== null){
// Here where I expect job to be dispatched (code above)
$runPackageRequest =
$this->client->request('POST', config('api.url') .'/api/bulk/run?token=' . $this->token,
[
'multipart' => [
[
'name' => 'package_id',
'contents' => $package->id
],
]
]);
// Here I'm receiving stream for some reason
dd($runPackageRequest->getBody());
if($runPackageRequest->getStatusCode()==200){
return redirect(url('/success'));
}
}
}
return back();
}
Could anyone advise me what is wrong here and what causes the issue?
Thank you!
Alright, it was really interesting. After echoing config('queue.default') in my contoller it appeared that it's value indeed sync nevertheless that I set everything correctly.
Then I assumed that maybe the reason in Laravel itself and its variables. Indeed in .env file from Laravel side QUEUE_DRIVER being set to sync. After I changed it to QUEUE_DRIVER=database everything started working as expected.
Hope that will help someone in future.

Resources