Better Ways to Test Repeated Laravel Jobs

In this article, we'll explore two improved methods to make testing same class dispatched Laravel jobs a breeze. While the standard way gets the job done, it's like searching for a needle in a haystack when something goes wrong. We'll go through two approaches that shed light on exactly what's causing issues, making troubleshooting a whole lot easier.

The Code Under Test

Before we dive in, let's take a quick look at the code we're dealing with:

// web.php
<?php
Route::get('dispatch-jobs', \ App\Http\Controllers\JobController::class);

// JobController.php
<?php

namespace App\Http\Controllers;

use App\Jobs\TestJob;

class JobController extends Controller
{
    public function __invoke()
    {
        TestJob::dispatch('John', 30, '[email protected]');
        TestJob::dispatch('Will', 20, '[email protected]');
    }
}

The Issue with the Standard Approach

The standard way of testing Laravel jobs using the Queue::assertPushed method is helpful, but it doesn't give you a clear picture of which job's parameters are causing trouble. It's like knowing something's wrong in your car but not where exactly. Good luck finding which job's parameter is invalid :).

<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Jobs\TestJob;
use Illuminate\Support\Facades\Queue;

class JobControllerTest extends TestCase
{
    /** @test */
    public function assert_two_jobs_were_dispatched(): void
    {
        Queue::fake();

        $response = $this->get('dispatch-jobs');

        Queue::assertPushed(TestJob::class, 2);

        // be aware this callable is called twice
        // and passes if at least one job returns true
        Queue::assertPushed(function (TestJob $job) {
            return $job->name === 'John' &&
                $job->age === 30 &&
                $job->email === '[email protected]';
        });

        Queue::assertPushed(function (TestJob $job) {
            return $job->name === 'Will' &&
                $job->age === 20 &&
                $job->email === '[email protected]';
        });
    }
}

Better Way 1: Pinpointing Issues

Here's where it gets better. We can use the assertContains method in a smarter way. This not only helps us know that something's off but also exactly which parameter is causing the issue. However there is still an issue of not knowing which job precisely caused the test to fail.

<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Jobs\TestJob;
use Illuminate\Support\Facades\Queue;

class JobControllerTest extends TestCase
{
    /** @test */
    public function assert_two_jobs_were_dispatched(): void
    {
        Queue::fake();

        $response = $this->get('dispatch-jobs');

    Queue::assertPushed(TestJob::class, 2);

        Queue::assertPushed(function (TestJob $job) {
            $this->assertContains(
                $job->name,
                ['John', 'Will'],
                // it outputs the following message if assertion fails
                "The name is {$job->name}, should be John or Will"
            );

            $this->assertContains(
                $job->age,
                [30, 20],
                "The age is {$job->age}, should be 30 or 20"
            );

            $this->assertContains(
                $job->email,
                ['[email protected]', '[email protected]'],
                "The email is {$job->email}, should be [email protected] or [email protected]"
            );

            return true;
        });
    }
}

Better Way 2: Zeroing in on Job & Parameters

If you want an even more detailed view of what's going wrong, this approach got you covered. This time, we'll use an array of assertion parameter sets and an $index passed to assertPushed by reference. I know, I know, passed by reference euggh, that's awful, but do we have a choice here? This approach not only lets you know when there's a problem but also highlights which job's parameters need your attention.

<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Jobs\TestJob;
use Illuminate\Support\Facades\Queue;

class JobControllerTest extends TestCase
{
    /** @test */
    public function assert_two_jobs_were_dispatched(): void
    {
        Queue::fake();

        $response = $this->get('dispatch-jobs');

        Queue::assertPushed(TestJob::class, 2);

        $index = 0;
        $assertions = [
            ['name' => 'John', 'age' => 30, 'email' => '[email protected]'],
            ['name' => 'Will', 'age' => 20, 'email' => '[email protected]'],
        ];

        Queue::assertPushed(function (TestJob $job) use (&$index, $assertions) {
            $this->assertSame(
                $assertions[$index]['name'],
                $job->name,
                "The name should be {$assertions[$index]['name']}"
            );

            $this->assertSame(
                $assertions[$index]['age'],
                $job->age,
                "The age should be {$assertions[$index]['age']}"
            );

            $this->assertSame(
                $assertions[$index]['email'],
                $job->email,
                "The age should be {$assertions[$index]['email']}"
            );

            $index++;

            return true;
        });
    }
}

Wrapping Up

So, the takeaway is this: while these testing approaches might not be the fanciest and for everybody, they sure help when you're testing dispatched Laravel jobs. Understanding the trade-offs will help you choose the method that makes your testing life easier.