How to test Laravel 5 jobs? - php

I try to catch an event, when job is completed
Test code:
class MyTest extends TestCase {
public function testJobsEvents ()
{
Queue::after(function (JobProcessed $event) {
// if ( $job is 'MyJob1' ) then do test
dump($event->job->payload());
$event->job->payload()
});
$response = $this->post('/api/user', [ 'test' => 'data' ], $this->headers);
$response->assertSuccessful($response->isOk());
}
}
method in UserController:
public function userAction (Request $request) {
MyJob1::dispatch($request->toArray());
MyJob2::dispatch($request->toArray());
return response(null, 200);
}
My job:
class Job1 implements ShouldQueue {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $data = [];
public function __construct($data)
{
$this->data= $data;
}
public function handle()
{
// Process uploaded
}
}
I need to check some data after job is complete but I get serialized data from
$event->job->payload() in Queue::after And I don't understand how to check job ?

Well, to test the logic inside handle method you just need to instantiate the job class & invoke the handle method.
public function testJobsEvents()
{
$job = new \App\Jobs\YourJob;
$job->handle();
// Assert the side effect of your job...
}
Remember, a job is just a class after all.

Laravel version ^5 || ^7
Synchronous Dispatching
If you would like to dispatch a job immediately (synchronously), you may use the dispatchNow method. When using this method, the job will not be queued and will be run immediately within the current process:
Job::dispatchNow()
Laravel 8 update
<?php
namespace Tests\Feature;
use App\Jobs\ShipOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Bus;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_orders_can_be_shipped()
{
Bus::fake();
// Perform order shipping...
// Assert that a job was dispatched...
Bus::assertDispatched(ShipOrder::class);
// Assert a job was not dispatched...
Bus::assertNotDispatched(AnotherJob::class);
}
}

This my generic method, using a route
Route::get('job-tester/{job}', function ($job) {
if(env('APP_ENV') == 'local'){
$j = "\\App\Jobs\\".$job;
$j::dispatch();
}
});

Related

How to override specific methods in Laravel core classes?

To be even more specific and provide the most basic example of one of the methods that I would like to override is the ScheduleRunCommand::handle() method.
(Specifically, would like to change the messaging for when nothing is ready to run to include a timestamp in the string.)
ScheduleRunCommand.php
public function handle(Schedule $schedule, Dispatcher $dispatcher, ExceptionHandler $handler)
{
$this->schedule = $schedule;
$this->dispatcher = $dispatcher;
$this->handler = $handler;
foreach ($this->schedule->dueEvents($this->laravel) as $event) {
if (! $event->filtersPass($this->laravel)) {
$this->dispatcher->dispatch(new ScheduledTaskSkipped($event));
continue;
}
if ($event->onOneServer) {
$this->runSingleServerEvent($event);
} else {
$this->runEvent($event);
}
$this->eventsRan = true;
}
if (! $this->eventsRan) {
$this->info('No scheduled commands are ready to run.');
}
}
Just to be clear, I do not want to edit this core file directly, just extend, specifically overriding that method with an updated message for when no events ran.
You have to create a new command that extends ScheduleRunCommand.
There, you extend the base command and you do whatever you want.
This command with automatically override the original command.
<?php namespace App\Console\Commands;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Contracts\Events\Dispatcher;
class ScheduleRunCommand extends \Illuminate\Console\Scheduling\ScheduleRunCommand
{
public function handle(Schedule $schedule, Dispatcher $dispatcher, ExceptionHandler $handler)
{
dd("test");
parent::handle($schedule, $dispatcher, $handler);
}
}
If you did all this correctly, running php artisan schedule:run will execute the handle function of your new class:
test#project:~/code$ php artisan schedule:run
"test"
To specifically answer your question, since you can't only override No scheduled commands are ready to run., you must copy / paste the entire handle function and modify the message directly:
<?php namespace App\Console\Commands;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Contracts\Events\Dispatcher;
class ScheduleRunCommand extends \Illuminate\Console\Scheduling\ScheduleRunCommand
{
public function handle(Schedule $schedule, Dispatcher $dispatcher, ExceptionHandler $handler)
{
$this->schedule = $schedule;
$this->dispatcher = $dispatcher;
$this->handler = $handler;
foreach ($this->schedule->dueEvents($this->laravel) as $event) {
if (! $event->filtersPass($this->laravel)) {
$this->dispatcher->dispatch(new ScheduledTaskSkipped($event));
continue;
}
if ($event->onOneServer) {
$this->runSingleServerEvent($event);
} else {
$this->runEvent($event);
}
$this->eventsRan = true;
}
if (! $this->eventsRan) {
$this->info('[.'.date('Y-m-d H:i:s').'] No scheduled commands are ready to run.');
}
}
}

Detecting if a job has been dispatched using dispatchNow

I have a job that under certain circumstances calls another job
<?php namespace App\Jobs;
use App\Models\Account;
class EnqueueScheduledDownloads implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $account;
public function __construct(Account $account)
{
$this->account = $account;
}
public function handle()
{
foreach($this->account->pending_downloads as $file)
{
DownloadFile::dispatch($file);
}
}
}
While the download job is usually executed in a queue; there are times, for example during testing, where it would make my life much easier if the whole chain was processed synchronously in a blocking fashion. I would like to be able to do something like this:
public function handle()
{
foreach($this->account->pending_downloads as $file)
{
if($this->getDispatchMode() == 'sync') {
DownloadFile::dispatchNow($file);
} else {
DownloadFile::dispatch($file);
}
}
}
Is this possible?
After a bit of poking around I was able to answer my own question. Yes it is possible; if a job is dispatched via dispatchNow() the job property of the Queueable object will be null, whereas if it is dispatched on a connection using dispatch() it will be set to an implementation of Illuminate\Contracts\Queue\Job. So the handle method can be changed as such:
public function handle()
{
foreach($this->account->pending_downloads as $file)
{
if(is_null($this->job)) {
DownloadFile::dispatchNow($file);
} else {
DownloadFile::dispatch($file);
}
}
}
And it will work as expected. I was able to find this solution by creating a new job:
<?php namespace App\Jobs;
class TestJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct()
{
}
public function handle()
{
dump(get_object_vars($this));
}
}
and dispatching it on various queue and connections as well as with dispatchNow() and observing the output. Furthermore it is possible to retreive the connection and queue the job was dispatched on from the $this->job:
public function handle()
{
echo $this->job->getConnectionName();
echo $this->job->getQueue();
}

Test Queue functionality?

According to the Laravel Documentation, I can use Queue::fake(); prevent jobs from being queued.
What is not clear how to test (PHPUnit) a few methods in the Job Class while it is not being queued.
For example:
class ActionJob extends Job
{
public $tries = 3;
protected $data;
public function __construct($data)
{
$this->data = $data;
}
public function handle()
{
if ($this->data['action'] == "deleteAllFiles") {
$this->deleteAllFiles();
}
}
protected function deleteAllFiles()
{
//delete all the files then return true
// if failed to delete return false
}
}
Here is example I want to test deleteAllFiles() - do I need to mock it?
The idea of using the fakes is that they're an alternative to mocking. So, yes, if you want to mock that deleteAllFiles() was called, then I don't believe you can do that with the fake.
However, you can assert that a certain attribute exists on the job.
One thing, it's not in your example, but make sure your job is implementing \Illuminate\Contracts\Queue\ShouldQueue.
Something like this
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class ActionJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $data; // Make sure this public so you can access it in your test
public function __construct($data)
{
$this->data = $data;
}
public function handle()
{
if ($this->data['action'] == "deleteAllFiles") {
$this->deleteAllFiles();
}
}
protected function deleteAllFiles()
{
// do stuff
}
}
Then in your test:
// ActionJobTest.php
Queue::fake();
// Do some things to set up date, call an endpoint, etc.
Queue::assertPushed(ActionJob::class, function ($job) {
return $job->data['action'] === 'deleteAllFiles';
});
If you want to assert on $data within the job, then you can make some other state change and assert on that in the Closure.
Side note: If the Job is Disptachable you can also assert like this:
// ActionJobTest.php
Bus::fake();
// Do some things to set up date, call an endpoint, etc.
Bus::assertDispatched(ActionJob::class, function ($job) {
return $job->data['action'] === 'deleteAllFiles';
});

Laravel integration testing jobs

I am trying to run an integration tests for my app. I have those jobs:
StartJob
PrepareJob
PeformJob
StartJob dispatches one or more PrepareJob, every PrepareJob dispatches one PerformJob.
Adding this
$this->expectsJobs(
[
StartJobs::class,
PrepareJob::class,
PerformJob::class
]
);
makes my test fail with error saying
1) JobsTest::testJobs
BadMethodCallException: Method Mockery_0_Illuminate_Contracts_Bus_Dispatcher::dispatchNow() does not exist on this mock object
Removing $this->expectsJobs makes all my tests pass, but I can't assert a given job was run, only whether it modified the DB to a given state.
StartJobs.php
class StartJobs extends Job implements ShouldQueue
{
use InteractsWithQueue;
use DispatchesJobs;
public function handle(Writer $writer)
{
$writer->info("[StartJob] Started");
for($i=0; $i < 5; $i++)
{
$this->dispatch(new PrepareJob());
}
$this->delete();
}
}
PrepareJob.php
class PrepareJob extends Job implements ShouldQueue
{
use InteractsWithQueue;
use DispatchesJobs;
public function handle(Writer $writer)
{
$writer->info("[PrepareJob] Started");
$this->dispatch(new PerformJob());
$this->delete();
}
}
PerformJob.php
class PerformJob extends Job implements ShouldQueue
{
use InteractsWithQueue;
public function handle(Writer $writer)
{
$writer->info("[PerformJob] Started");
$this->delete();
}
}
JobsTest.php
class JobsTest extends TestCase
{
/**
* #var Dispatcher
*/
protected $dispatcher;
protected function setUp()
{
parent::setUp();
$this->dispatcher = $this->app->make(Dispatcher::class);
}
public function testJobs()
{
$this->expectsJobs(
[
StartJobs::class,
PrepareJob::class,
PerformJob::class
]
);
$this->dispatcher->dispatch(new StartJobs());
}
}
I think it has to do something with how I am using a concrete dispatcher, while $this->expectsJob mocks the dispatcher. Might be related to this - https://github.com/laravel/lumen-framework/issues/207. What's the way to solve this?
To me it sounds like there is no dispatchNow()-method. In the Jobs your run dispatch() but the error says dispatchNow() does not exist.
Laravel didn't have the dispatchNow()-method before a certain version (i think Laravel 5.2 ... not sure) but just the dispatch(). Could be that the expectsJobs didn't think about that and fails.
You could try not passing it in one array but use 3 commands:
$this->expectsJobs(StartJobs::class);
$this->expectsJobs(PrepareJob::class);
$this->expectsJobs(PerformJob::class);
Maybe that helps.

Laravel 5.1 event fire

I'm firing event and passing object with array like this:
$event = new App\Events\SendMessage;
$event->msg = [ 'sender_id'=>'191',
'recepient_id'=>'190',
'text'=>'some text',
];
Event::fire($event);
Is it possible to make this call a bit shorter and fire event in one line like this?
Event::fire(new App\Events\SendMessage([
'sender_id'=>'191',
'recepient_id'=>'190',
'text'=>'some text',
]));
You would just need to make sure your event constructor is setup to populate that field.
See: http://laravel.com/docs/5.1/events#defining-events
<?php
namespace App\Events;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class SendMessage extends Event
{
use SerializesModels;
public $msg;
public function __construct($msg)
{
$this->msg = $msg;
}
}
Yep. Just pass the data in the __construct()
class SendMessage extends Event
{
protected $data;
public function __construct(array $data)
{
$this->data = $data;
}
}
In your App\Events\SendMessage you need to define a constructor method for example:
namespace App\Events;
class SendMessage {
protected $data = null;
public function __construct(Array $data)
{
$this->data = $data;
}
}
you can fire event like this in laravel
just put the code into your controller
event(new App\Events\EventClassName());
if your Event has parameters then
event(new App\Events\EventClassName(['first' => 'value','second' => 'value']));

Categories