How to test then batch method in Laravel

Intro

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.

The Code Under Test

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

Testing the Batch Controller

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

Testing Batch Job Dispatch

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.

Testing the 'then' Method

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.

Testing the finally and catch Methods

Just 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.

Conclusion

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.