Testing jobs in Laravel

Laravel, Testing, Queue

Recently someone in a Slack channel i’m a member of asked the following:

"I want to test if job A fires job B, but how do you test something like that?"

- Niek ten Hoopen

Especially when the queue is set up to be run asychronus this seems impossible to test. The normal flow would be something like this:

Controller -> job A -> job B

Now how do you test this? The answer is pretty simple: You test job A separate from your controller and job B.

But how do you call a job? That is also pretty simple: A job is just another class, and this calls can be initiated as well. The only difficult part in here is that in opposite to almost every other class you have 2 places where you can define dependencies:

  • The constructor - That is used for dependencies specific for this job. A specific User model for example.

  • The handle method - Here you can define dependencies you would normally place in your constructor. The SDK for an api for example.

The constructor is easy: That is the point where you put in you prepared mocks to influence the test.

The handle method is harder. You can initiate the dependencies yourself and inject them, but this makes your test fragile. What is a dependency is added or removed? Or if the arguments of any of the dependencies is changed? Your test would break, although it probably is not connected to the job. Normally we use the container for this. But can we use it in this case? Yes!

Using the container

We can use the app()->call() method to call the method of our job:

$instance = resolve(CallThisApiJob::class, ['user' => $user, 'customer' => $customer]);
app()->call([$instance, 'handle']);

In this example we use the container to create an instance of the CallThisApiJob. Next we use the app()->call() method to call the handle method of our job instance. This also fullfills all the dependencies this method has.

Writing the test

So now we have this info, how does our test looks like? Well, something like this:

public function testTheSecondJobIsPushed()
{
    \Queue::fake();

    $user = factory(\App\Models\User::class)->create();
    $user->isAdmin = true;
    $customer = factory(\App\Models\Customer::class)->create();

    $instance = resolve(CallThisApiJob::class, ['user' => $user, 'customer' => $customer]);
    app()->call([$instance, 'handle']);

    \Queue::assertPushed(SecondJob::class);
}

Thanks to the \Queue::fake() the SecondJob is never fired, and can we test if it under normal circumstances would be pushed and thus processed. This way we can test out job without the risk that the second job is actually fired.

Want to respond?