Tips for testing queued jobs in Laravel

Mary-Kate Olsen
Release: 2024-09-19 22:20:33
Original
459 people have browsed it

When working with Laravel applications, it’s common to encounter scenarios where a command needs to perform an expensive task. To avoid blocking the main process, you might decide to offload the task to a job that can be processed by a queue.

Let’s walk through an example. Imagine the command app:import-users needs to read a large CSV file and create a user for each entry. Here’s what the command might look like:

/* ImportUsersCommand.php */

namespace App\Console\Commands;

/*...*/

class ImportUsersCommand extends Command
{
    protected $signature = 'app:import-users';

    public function handle()
    {
        dispatch(new ImportUsersJob());

        $this->line('Users imported successfully.');
        $this->line('There are: ' . User::count(). ' Users.');
    }
}
Copy after login

In this example, the command dispatches a job to handle the reading of the file and the creation of users. Here’s how the ImportUsersJob.php might look:

/* ImportUsersJob.php */

namespace App\Jobs;

/*...*/

class ImportUsersJob implements ShouldQueue
{
    public function handle(FileReader $reader): void
    {   
        foreach($reader->read('users.csv') as $data) {
            User::create([
                'name' => $data['name'], 
                'email' => $data['email'],
            ]);
        }
    }
}
Copy after login

When testing this feature, a typical test for the command might look like this:

/* ImportUsersCommandTest.php */

namespace Tests\Feature;

/*...*/

class ImportUsersCommandTest extends TestCase
{
    use RefreshDatabase;

    public function test_it_processes_the_file(): void
    {
        Storage::fake('local')->put('users.csv', "...");

        $this->artisan('app:import-users')
            ->expectsOutput('Users imported successfully.')
            ->expectsOutput('There are: 10 Users.')
            ->assertSuccessful();

        $this->assertDatabaseCount('users', 10);
    }
}
Copy after login

At first glance, this test seems to work perfectly. Running the test suite shows a successful result:

Tips for testing queued jobs in Laravel

Real World Execution

However, when you run the app:import-users command in a real environment, you might get an unexpected result:

Tips for testing queued jobs in Laravel

As you can see, the command output indicates that there are 0 users in the database. So, Why does this happen?

The reason is that the job is dispatched to a queue, so it doesn’t run synchronously with the command execution. The users will be created only when the queue processes the job later.

Why does the test pass?

The test suite uses the sync queue driver by default, meaning jobs are processed synchronously during the test. As a result, the job runs immediately, giving the idea that everything works as expected.

While this behavior is acceptable in the test environment, it’s important to recognize that real-world results depend on the QUEUE_CONNECTION configuration in your production environment. And given your project requirements, you might know that the job will be processed in an async queue.

Once you’re aware of this distinction, you may want to improve your tests to avoid “false positives”.

Testing your job is dispatched

First, it’s important to verify that the command actually dispatches the job, regardless of whether the job is processed synchronously or asynchronously. Here’s how to test that:

/* ImportUsersCommandTest.php */

namespace Tests\Feature;

/*...*/

class ImportUsersCommandTest extends TestCase
{    
    public function test_it_dispatches_the_job(): void
    {
        Queue:fake();

        $this->artisan('app:import-users')
            ->expectsOutput('Process has been queued.')
            ->assertSuccessful();

        Queue::assertPushed(ImportUsersJob::class);
    }
}
Copy after login

Testing your job is processed

Once you’ve confirmed that the job is dispatched, you can test the actual work performed by the job in a separate test. Here’s how you might structure the test for the job:

/* ImportUsersJobTest.php */

namespace Tests\Feature;

/*...*/

class ImportUsersJobTest extends TestCase
{
    use refreshDatabase;

    public function test_it_processes_the_file()
    {
        Storage::fake('local')->put('users.csv', "...");

        app()->call([new ImportUsersJob(), 'handle']);

        $this->assertDatabaseCount('users', 10);
    }
}

Copy after login

This ensures that the job performs the necessary work, regardless of whether it’s processed by a queue or synchronously.

Handling edge cases

As in real life, edge cases might happen and you should be ready for these.

Laravel’s queue system, according to your workers configuration, will retry jobs when an exception occurs, and if retries are exceeded, the job will be marked as failed.

So, what happens if the file doesn’t exist? You need to handle such edge cases by validating inputs and throwing exceptions when necessary.

Here’s how you might handle this in your job:

/* ImportUsersJobTest.php */

namespace App\Jobs;

/*...*/

class ImportUsersJob implements ShouldQueue
{
    use Queueable;

    public function handle(FileReader $reader): void
    {   
        if(!Storage::disk('local')->exists('users.csv')){
            throw new Exception('The users.csv file doesn\'t exist.')
        }

        foreach($reader->read('users.csv') as $data) {
            User::create([
                'name' => $data['name'], 
                'email' => $data['email'],
            ]);
        }
    }
}
Copy after login

Here’s how you’d test this scenario:

/* ImportUsersJobTest.php */

namespace Tests\Feature;

/*...*/

class ImportUsersJobTest extends TestCase
{
    use refreshDatabase;

    /*...*/

    public function test_it_fails_when_file_doesnt_exist(): void
    {
        Storage::fake('local');

        $this->expectException(Exception::class);
        $this->expectExceptionMessage('The users.csv file doesn\'t exist.');

        dispatch(new ImportUsersJob());
    }
}

Copy after login

Final thoughts

This approach ensures that your tests more accurately reflect how jobs will be processed in the real world.
The same strategy can be applied when a controller dispatches a job to a queue or where a event listener is queued.
As always, adjust these practices to fit your project and team.

I’d love to hear your thoughts!

The above is the detailed content of Tips for testing queued jobs in Laravel. For more information, please follow other related articles on the PHP Chinese website!

source:dev.to
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Latest Articles by Author
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template