In this blog post, we'll explore how to test Laravel jobs, specifically focusing on asserting that a batch of jobs has been dispatched and testing that a then
method is called when all the batched jobs are completed.
We have a BatchController
that dispatches two TestJob
instances and, upon their completion, triggers a BatchCompletedJob
. Here's the code:
// BatchController.php
<?php
namespace App\Http\Controllers;
use App\Jobs\TestJob;
use Illuminate\Bus\Batch;
use App\Jobs\BatchCompletedJob;
use Illuminate\Support\Facades\Bus;
class BatchController extends Controller
{
public function __invoke()
{
Bus::batch([
new TestJob('first'),
new TestJob('second'),
])->then(function ($batch) {
BatchCompletedJob::dispatch();
})->dispatch();
}
}
// TestJob.php
<?php
namespace App\Jobs;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class TestJob implements ShouldQueue
{
use Dispatchable, Queueable, Batchable, InteractsWithQueue;
public function __construct(public string $name)
{
}
public function handle(): void
{
info($this->name);
}
}
// BatchCompletedJob.php
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class BatchCompletedJob implements ShouldQueue
{
use Dispatchable, Queueable, InteractsWithQueue;
public function handle(): void
{
}
}
// web.php
Route::get('batch', \App\Http\Controllers\BatchController::class);
Now, let's dive into testing the BatchController
using PHPUnit.
// BatchControllerTest.php
<?php
namespace Tests\Feature;
use App\Jobs\BatchCompletedJob;
use Tests\TestCase;
use Illuminate\Bus\PendingBatch;
use Illuminate\Support\Facades\Bus;
class BatchControllerTest extends TestCase
{
public function assert_batch_of_jobs_was_dispatched(): void
{
Bus::fake();
$this->get('batch');
Bus::assertBatched(function (PendingBatch $batch) {
$this->assertCount(2, $batch->jobs);
$this->assertSame('first', $batch->jobs[0]->name);
$this->assertSame('second', $batch->jobs[1]->name);
return true;
});
}
public function asssert_then_method_is_called(): void
{
Bus::fake();
$this->get('batch');
// assert, then the method is called and executed
Bus::batched(function (PendingBatch $batch) {
// get then callback
[$thenCallback] = $batch->thenCallbacks();
// excecute it
$thenCallback->getClosure()->call($this, $this);
// make desired assertions db record is saved, event or job is dispatched
Bus::assertDispatched(BatchCompletedJob::class, 1);
return true;
});
}
}
In the first test method, assert_batch_of_jobs_was_dispatched
, we want to make sure that the batch of jobs is dispatched correctly. This assertion is straight-forward one.
The second test method, asssert_then_method_is_called
, is a bit trickier and it is the one where I've spend half a day looking for solution. It aims to test the then
method within the batch. Here's a breakdown of what's happening: We use Bus::batched
to assert that the batch has been processed.
Inside this assertion, we access the then
callback and execute it using call($this, $this)
. This ensures that the then
method is called and executed.
We then make further assertions, such as checking if the BatchCompletedJob
has been dispatched.
finally
and catch
MethodsJust as we've explored testing the then
method, the same principles apply when you want to test the finally
and catch
methods in Laravel batch jobs.
When dealing with the finally
method, you can use a similar approach to the one we've demonstrated for the then
method. Access the finally callback with finallyCallbacks()
method within your batched job, execute it, and make the necessary assertions.
Similarly, for the catch
method. Access the catch callback with catchCallbacks()
method, execute it and make assertions.
Testing Laravel batch jobs, especially when dealing with the then
method, can be a bit tricky. In this blog post, we've demonstrated how to test both the dispatching of a batch of jobs and the execution of the then
method. Finally a special thanks to @coderjono for proposing a solution in this Laracast forum thread.