cosminc's avatar

Trying to write a unit test for a service that dispaches a job

Hello,

I'm currently implementing an API (using Laravel 8.26.1) that has an endpoint which receives an array of e-mails and sends those e-mails asynchronously. Everything is working fine so far, but I'm willing to write a couple of unit tests for this behavior.

First, I have a controller with a send() method that calls a service like so:

class MyController extends Controller 
{
    private $service;

    public function __construct(MyService $service) {
        $this->service = $service;
    }

    public function send(Request $request) {
        $this->service->send($request->validated());
    }
}

Then the service has a single method send() which dispatches a job:

class MyService 
{    
    public function send($messages) {
        dispatch(new MyJob($messages));
    }
}

And finally the job that sends the e-mail:

class MyJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    private $messages;

    public function __construct($messages) {
        $this->messages = $messages;
    }

    public function handle() {
        foreach($this->messages as $message) {
            Mail::to($message['recipient'])->send(new MyMail($message));
        }
    }
}

Given all the structure above, my test so far looks like this:

class MyTest extends TestCase
{
    public function test_it_dispatches_job()
    {
        Queue::fake();

		$messages = [];
       
        (new MyService())->send($messages);

        Queue::assertPushed(MyJob::class);

    }
}

When I run the test I get the following error:

Illuminate\Contracts\Container\BindingResolutionException : Target [Illuminate\Contracts\Bus\Dispatcher] is not instantiable.

How can I update my code so I can test the behavior?

I also tried to dispatch the job in the service like MyJob::dispatch($messages) but I got the same error.

Thanks!

0 likes
8 replies
mabdullahsari's avatar

Are you sure the app has fully bootstrapped before this test is executed? Seems like there is no container binding for the Dispatcher. Can you show the base TestCase?

cosminc's avatar

What do you mean by base TestCase? If you're talking about the abstract class that's extended by the MyTest class, then it's the default one that ships with Laravel.

mabdullahsari's avatar

Is Illuminate\Bus\BusServiceProvider::class among the registered service providers in the app config? If yes, then I have no clue sorry. The snippets you've provided seem to be OK.

cosminc's avatar

Yes, Illuminate\Bus\BusServiceProvider::class is in the providers array.

cosminc's avatar

Found the problem! It wasn't working because of a wrong import. I had use PHPUnit\Framework\TestCase; instead of use Tests\TestCase;.

martinbean's avatar

@cosminc Yes, because what you have isn’t a unit test. It’s an integration test.

If your test needs access to external resources like the framework, a database, etc then it’s not a unit test. If you can’t get your test to run extending PHPUnit’s TestCase class, then that’s a good indication your test isn’t a unit test as well.

1 like
cosminc's avatar

@martinbean, I see, but how should a unit test look like if I want to test that the method from within the service is working as expected? Thanks.

martinbean's avatar

@cosminc Interacting with a queue is outside the scope of a unit test. You’re not testing a single unit of code; you’re testing how stuff interacts with external services like a message queue.

A unit test should test a single unit of your application only, i.e. a method. A unit test can test a method by passing arguments to it, and asserting the result. If you need anything else (a database, a message queue, an IoC container) then it’s no longer a unit test.

Please or to participate in this conversation.