Laravel integration testing jobs - php

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.

Related

Is it bad design to call a controller method from a job class

I recently wanted to implement queue functionality to my Laravel project, and as of now, it works. However, I'm not sure what the proper design pattern for a solution like this is since I'm new to Laravel.
I have a sync() method inside a ProductController class which is a void method that calls to an API, gets products, and inserts/updates records in a database. Since it takes around 2-5 minutes for the function to execute, I decided to try and implement a job to do it in the background.
I wasn't sure whether to copy the whole method and paste it into the handle() method inside the "SyncProducts" job class or call it from the controller class.
As of now, my job class looks like this.
class SyncProducts implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*
* #return void
*/
public $timeout = 1800;
public function __construct()
{
//
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
try {
(new \App\Http\Controllers\ProductController)->sync();
} catch (Exception $e) {
} catch (ResourceException $e) {
}
}
}
Inside the ProductController class, I added a new method that dispatches the job and redirects the user.
public function syncRun()
{
SyncProducts::dispatch();
return back();
}
Is this bad design? What is the proper way to implement it?
I consider it as bad design, and instead would use an action class which holds the logic in a re-usable way. The action class then can be called from a controller or from a job.
Here a (fairly basic) overview, for the sole purpose of giving an idea about the concept:
class MyWhateverAction
{
public function __construct($data) {
// whatever you need
}
public function execute()
{
// the logic which you now have in the controller
}
}
class MyWhateverController
{
public function synch($request, MyWhateverAction $action)
{
// do something to set $data
$action->execute($data)
return // whatever you need
}
}
class MyWhateverJob
{
public function handle($data, MyWhateverAction $action)
{
$action->execute($data)
}
}
More detailed infos about it:
a) https://stitcher.io/blog/laravel-queueable-actions
b) https://twitter.com/mmartin_joo/status/1509181862014509065?s=21

Factory helper not working within PHPUnit setup method

Writing some unit tests, and I want to have an object created before the tests in the class are done. So I set up the setUpBeforeClass() method:
<?php
namespace Tests\Unit;
use Tests\TestCase;
use App\Location;
class UserTests extends TestCase {
const FAKEID = 9999999;
public static function setUpBeforeClass() : void {
parent::setUpBeforeClass();
factory(Location::class)->make(["id" => self::FAKEID])->save();
}
}
But when I try running this, I get this error:
InvalidArgumentException: Unable to locate factory with name [default] [App\Location].
But the factory class is set up properly. In fact, if I move this same line down to one of my test functions it works perfectly.
public function testCreateUser() {
factory(Location::class)->make(["id" => self::FAKEID])->save();
// do other stuff...
}
The only thing that sticks out to me as different about setUpBeforeClass() is that it's a static method, but I don't know why that would prevent the factory class from working.
Laravel does a lot of setting up in the setUp() method in the TestCase class. The setUpBeforeClass() method is called before that, that's why your factory is not loaded yet.
The Laravel's TestCase class setup method (see class):
/**
* Setup the test environment.
*
* #return void
*/
protected function setUp()
{
if (! $this->app) {
$this->refreshApplication();
}
$this->setUpTraits();
foreach ($this->afterApplicationCreatedCallbacks as $callback) {
call_user_func($callback);
}
Facade::clearResolvedInstances();
Model::setEventDispatcher($this->app['events']);
$this->setUpHasRun = true;
}
Change your code to use setUp instead:
protected static function setUp() : void
{
parent::setUp();
factory( Location::class )->make( ["id" => self::FAKEID] )->save();
}

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

How to wrap whole class in transaction in PHPUnit?

My idea is to specify in each test class parameters, depends how we want to wrap test in transactions. I use Laravel 5.1.
use DB;
class TestCase
{
protected static $wrapMethodInTransaction = true;
protected static $wrapClassInTransaction = false;
/**
* #beforeClass
*/
public static function isClassWrappedInTransactionBefore()
{
if (!static::$wrapMethodInTransaction && static::$wrapClassInTransaction) {
DB::beginTransaction();
}
}
/**
* #afterClass
*/
public static function isClassWrappedInTransactionAfter()
{
if (!static::$wrapMethodInTransaction && static::$wrapClassInTransaction) {
DB::rollback();
}
}
}
The case is that #beforeClass & #afterClass fires when application is not booted yet/already dead, thus class DB is not accesible. I just want to run those methods once per class, but when application is booted/not killed yet. Are there any solutions for this?
DatabaseTransactions trait does not do the work, because it starts transaction after setUp() and rollbacks it after tearDown(). Laravel's 'magic'. I want the setUp() to be in transaction.

Laravel Unit Testing Dependency Injection

I'm trying to write a test class for a shopping cart. Here is what I have:
ShoppingCartTest.php
class ShoppingCartTest extends TestCase {
use DatabaseTransactions;
protected $shoppingCart;
public function __construct() {
$this->shoppingCart = resolve('App\Classes\Billing\ShoppingCart');
}
/** #test */
public function a_product_can_be_added_to_and_retrieved_from_the_shopping_cart() {
// just a placeholder at the moment
$this->assertTrue(true);
}
}
However, when I run phpunit, it seems like Laravel is unable to resolve my ShoppingCartClass.
Here is the error:
Fatal error: Uncaught exception 'Illuminate\Contracts\Container\BindingResolutionException'
with message 'Unresolvable dependency resolving
[Parameter #0 [ <required> $app ]] in class Illuminate\Support\Manager'
in C:\Development Server\EasyPHP-Devserver-16.1\eds-www\nrponline\vendor\laravel\framework\src\Illuminate\Container\Container.php:850
I have my ShoppingCart class being resolved in a number of different controllers just fine.
Why can't Laravel resolve it during my tests?
I refered to this post as well but still didn't have any luck.
I figured it out. Here is the updated class.
class ShoppingCartTest extends TestCase {
use DatabaseTransactions;
protected $shoppingCart;
public function setUp() {
parent::setUp();
$this->shoppingCart = $this->app->make('App\Classes\Billing\ShoppingCart');
}
/** #test */
public function a_product_can_be_added_to_and_retrieved_from_the_shopping_cart() {
// just a placeholder at the moment
$this->assertTrue(true);
}
}
Thanks to #edcs for guiding me in the right direction.
You need to use a setUp function and not __construct as the app instance hasn't been created yet.
If you want to use __construct you have to use the same constructor of PHPUnit\Framework\TestCase and remember to call the parent method if you don't want to break anything
class MyTest extends TestCase
{
public function __construct($name = null, array $data = [], $dataName = '')
{
parent::__construct($name, $data, $dataName);
// my init code
}
}
However the proper way would be to use the method setUpBeforeClass() if you want to execute your init code once or setUp() if you want to execute the init code before each test contained in your class.
Check PHPUnit documentation for more details.

Categories