Test Queue functionality? - php

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

Related

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

How to test Laravel 5 jobs?

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

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 return JSON or View depends if ajax or not

I would like to know if there is a magic method to use this scenario :
If I call a page via an AJAX request the controller returns a JSON object, otherwise it returns a view, i'm trying to do this on all my controllers without changin each method.
for example i know that i can do this :
if (Request::ajax()) return compact($object1, $object2);
else return view('template', compact($object, $object2));
but I have a lot of controllers/methods, and I prefer to change the basic behavior instead of spending my time to change all of them. any Idea ?
The easiest way would be to make a method that is shared between all of your controllers.
Example:
This is your controller class that all other controllers extend:
<?php namespace App\Http\Controllers;
use Illuminate\Routing\Controller as BaseController;
abstract class Controller extends BaseController
{
protected function makeResponse($template, $objects = [])
{
if (\Request::ajax()) {
return json_encode($objects);
}
return view($template, $objects);
}
}
And this is one of the controllers extending it:
<?php namespace App\Http\Controllers;
class MyController extends Controller
{
public function index()
{
$object = new Object1;
$object2 = new Object2;
return $this->makeResponse($template, compact($object, $object2));
}
}
Update for Laravel 5+
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
protected function makeResponse($request, $template, $data = [])
{
if ($request->ajax()) {
return response()->json($data);
}
return view($template, $data);
}
}
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class MyController extends Controller
{
public function index(Request $request)
{
$object = new Object1;
$object2 = new Object2;
return $this->makeResponse($request, $template, compact($object, $object2));
}
}
There is no magic but you can easily override ViewService in 3 steps:
1.create your view factory (your_project_path/app/MyViewFactory.php)
<?php
/**
* Created by PhpStorm.
* User: panos
* Date: 5/2/15
* Time: 1:35 AM
*/
namespace App;
use Illuminate\View\Factory;
class MyViewFactory extends Factory {
public function make($view, $data = array(), $mergeData = array())
{
if (\Request::ajax()) {
return $data;
}
return parent::make($view, $data, $mergeData);
}
}
2.create your view service provider (your_project_path/app/providers/MyViewProvider.php)
<?php namespace App\Providers;
use App\MyViewFactory;
use Illuminate\View\ViewServiceProvider;
class MyViewProvider extends ViewServiceProvider {
/**
* Register the application services.
*
* #return void
*/
public function register()
{
parent::register();
}
/**
* Overwrite original so we can register MyViewFactory
*
* #return void
*/
public function registerFactory()
{
$this->app->singleton('view', function($app)
{
// Next we need to grab the engine resolver instance that will be used by the
// environment. The resolver will be used by an environment to get each of
// the various engine implementations such as plain PHP or Blade engine.
$resolver = $app['view.engine.resolver'];
$finder = $app['view.finder'];
// IMPORTANT in next line you should use your ViewFactory
$env = new MyViewFactory($resolver, $finder, $app['events']);
// We will also set the container instance on this view environment since the
// view composers may be classes registered in the container, which allows
// for great testable, flexible composers for the application developer.
$env->setContainer($app);
$env->share('app', $app);
return $env;
});
}
}
3.in your_project_path/config/app.php:
change 'Illuminate\View\ViewServiceProvider',
to 'App\Providers\MyViewProvider',
What this do:
it tells your application to use another view provider which will register your view factory
$env = new MyViewFactory($resolver, $finder, $app['events']);
in line 33 of MyViewProvider.php which will check if request is AJAX and return if true or continue with original behavior
return parent::make($view, $data, $mergeData);
in MyViewFactory.php line 19
Hope this help you,
In laravel 5.1, this is the best way:
if (\Illuminate\Support\Facades\Request::ajax())
return response()->json(compact($object1, $object2));
else
return view('template', compact($object, $object2));
The solution suggested by #ryanwinchester is really good. I, however, wanted to use it for the responses from update() and delete(), and there naturally return view() at the end doesn't make a lot of sense as you mostly want to use return redirect()->route('whatever.your.route.is'). I thus came up with that idea:
// App\Controller.php
/**
* Checks whether request is ajax or not and returns accordingly
*
* #param array $data
* #return mixed
*/
protected function forAjax($data = [])
{
if (request()->ajax()) {
return response()->json($data);
}
return false;
}
// any other controller, e.g. PostController.php
public function destroy(Post $post)
{
// all stuff that you need until delete, e.g. permission check
$comment->delete();
$r = ['success' => 'Wohoo! You deleted that post!']; // if necessary
// checks whether AJAX response is required and if not returns a redirect
return $this->forAjax($r) ?: redirect()->route('...')->with($r);
}

What is the difference between App\Events and App\Handlers\Events in Laravel?

I was watching this lesson and was trying to figure out which directory to put the EmailNotifier class file since it's an Event.
I don't know if it belongs in App\Events or App\Handlers\Events.
This is what I currently have:
<?php namespace App\Mailers;
use Illuminate\Mail\Mailer as Mail;
abstract class Mailer {
private $mail;
function __construct(Mail $mail)
{
$this->mail = $mail;
}
public function sendTo($user, $subject, $view, $data)
{
$this->mail->queue($view, $data, function ($message) use ($user, $subject)
{
$message->to($user->email)->subject($subject);
});
}
}
<?php namespace App\Mailers;
use App\User;
class UserMailer extends Mailer {
/**
* #param User $user
*/
public function sendWelcomeMessageTo(User $user)
{
$subject = 'Welcome To Backstage!';
$view = 'emails.registeration.confirm';
$data = [];
return $this->sendTo($user, $subject, $view, $data);
}
}
<?php namespace App\Handlers\Events;
class EmailNotifier extends Event {
private $mailer;
public function __construct(UserMailer $mailer)
{
$this->mailer = $mailer;
}
public function whenUserHasRegistered(UserHasRegistered $event)
{
$this->mailer->sendWelcomeMessageTo($event->user);
}
}
<?php namespace App\Events;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class UserHasRegistered extends Event {
use SerializesModels;
/**
* Create a new event instance.
*
* #return void
*/
public function __construct()
{
//
}
}
That's more of a discretionary concern. You generally want to categorize similar purpose items into the same namespace. Handlers\Events sounds like a place to put event handlers maybe, or perhaps it's a place for events that stem from handlers.
It sounds like you are on the right track placing an event in an Events namespace. Convention and consistency is the key. It doesn't matter so much as to what the final namespace is, just as long as it is consistent. IMO a more logical approach would be to have App\Event for all of your events and potentially sub namespace it from there for event categories. Handlers would be more self explanatory if they were somewhere like App\EventHandler and again sub namespaced into groups as needed.
That way it is pretty clear to an outsider who may need to work with your code in the future. That's my two cents as far as a general organization structure goes.
With deeper context into Laravel as the link laracasts.com implies. The App\Event namespace is for events which is what your EmailNotifier looks to be, where App\Handlers\Events is generally for handlers, subscribers, listeners, whatever you want to call them.

Categories