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.'); } }
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'], ]); } } }
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); } }
At first glance, this test seems to work perfectly. Running the test suite shows a successful result:
However, when you run the app:import-users command in a real environment, you might get an unexpected result:
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.
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”.
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); } }
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); } }
This ensures that the job performs the necessary work, regardless of whether it’s processed by a queue or synchronously.
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'], ]); } } }
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()); } }
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!