riseabove2_2's avatar

How do you test event subscribers?

I need to test an event subscriber to make sure it sends and email. I see quite a bit on information on testing event listeners in general, but I don't see anything about testing event subscribers. I thought since it is listening to an event, the same tests still apply. However, it doesn't work for me. I always get either Call to undefined method App\Events\ApplicationWasApprovedForInterview::listen() or event does not have the listener attached to it I was looking at https://laracasts.com/discuss/channels/testing/best-way-to-test-event-listeners and https://laravel.com/docs/9.x/mocking#event-fake

Event::assertListening(
    OrderShipped::class,
    SendShipmentNotification::class
);

among other resources.

My situation uses a nova action to dispatch the event. Action:

				event(
                    new ApplicationWasApprovedForInterview(
                        $application, 
                        isset($fields->send_email) ? $fields->send_email : true
                    )
                );

Listener:

class ApplicationEventSubscriber {
...
    public function subscribe($events)
    {
        ...
        $events->listen(
            ApplicationWasApprovedForInterview::class,
            'App\Listeners\ApplicationEventSubscriber@notifyWhenApplicationWasApprovedForInterview'
        );
    }

    public function notifyWhenApplicationWasApprovedForInterview(ApplicationWasApprovedForInterview $event) {
        $this->sendEmail($event);
    }

All this code was already written and currently works, but there are no tests. I need to test the event is dispatched and sends the email properly, but I can't even get it to assert the listener...

Tests: Testing the event dispatches works fine:

test_application_approve_for_interview_event_dispatched()
    {
        Event::fake([
            ApplicationWasApprovedForInterview::class
        ]);

        $application = ApplicationTestFactory::createApplication();

        $this->assertTrue($application->acceptedStatus === 'Pending');

        $action = new ApproveForInterview();
        $action->handle(new ActionFields(collect([]), collect()), collect([$application]));

        $this->assertNotNull($application->approved_for_interview_at);

         // Assert that an event was dispatched...
        Event::assertDispatched(ApplicationWasApprovedForInterview::class);
    }

This was my last attempt on testing the listener

public function test_application_approve_for_interview_event_listener()
    {
        $application = ApplicationTestFactory::createApplication();

        $application->approved_for_interview_at   = null;

        $listener = app()->make(ApplicationEventSubscriber::class);

        $event = new ApplicationWasApprovedForInterview($application, true);

        $listener->subscribe($event);

        // Event::assertListening(
        //     ApplicationWasApprovedForInterview::class, ApplicationEventSubscriber::class
        // );
    }

This returns

Call to undefined method App\Events\ApplicationWasApprovedForInterview::listen()
0 likes
3 replies
LaryAI's avatar
Level 58

To test an event subscriber in Laravel, you need to ensure that the event is dispatched and the subscriber's method is called correctly. Here's a step-by-step guide to achieve this:

  1. Mock the event and the subscriber: Use Laravel's Event::fake() to fake the event and then assert that the event was dispatched.
  2. Mock the method in the subscriber: Use PHPUnit's mocking capabilities to ensure that the subscriber's method is called when the event is dispatched.

Here's how you can structure your test:

Step 1: Test Event Dispatching

This part of your test is already correct. You are ensuring that the event is dispatched when the action is performed.

public function test_application_approve_for_interview_event_dispatched()
{
    Event::fake([
        ApplicationWasApprovedForInterview::class
    ]);

    $application = ApplicationTestFactory::createApplication();

    $this->assertTrue($application->acceptedStatus === 'Pending');

    $action = new ApproveForInterview();
    $action->handle(new ActionFields(collect([]), collect()), collect([$application]));

    $this->assertNotNull($application->approved_for_interview_at);

    // Assert that an event was dispatched...
    Event::assertDispatched(ApplicationWasApprovedForInterview::class);
}

Step 2: Test Event Subscriber

To test the subscriber, you need to mock the method that should be called when the event is dispatched. Here's how you can do it:

public function test_application_approve_for_interview_event_listener()
{
    Event::fake();

    $application = ApplicationTestFactory::createApplication();

    $application->approved_for_interview_at = null;

    // Mock the subscriber
    $subscriber = $this->getMockBuilder(ApplicationEventSubscriber::class)
                       ->onlyMethods(['notifyWhenApplicationWasApprovedForInterview'])
                       ->getMock();

    // Expect the method to be called once
    $subscriber->expects($this->once())
               ->method('notifyWhenApplicationWasApprovedForInterview')
               ->with($this->isInstanceOf(ApplicationWasApprovedForInterview::class));

    // Register the mock subscriber
    $this->app->instance(ApplicationEventSubscriber::class, $subscriber);

    // Dispatch the event
    event(new ApplicationWasApprovedForInterview($application, true));

    // Assert that the event was dispatched
    Event::assertDispatched(ApplicationWasApprovedForInterview::class);
}

Explanation:

  1. Mock the Subscriber: We create a mock of the ApplicationEventSubscriber class and specify that we only want to mock the notifyWhenApplicationWasApprovedForInterview method.
  2. Set Expectations: We set an expectation that the notifyWhenApplicationWasApprovedForInterview method should be called exactly once with an instance of ApplicationWasApprovedForInterview.
  3. Register the Mock: We register the mock subscriber in the service container using $this->app->instance().
  4. Dispatch the Event: We dispatch the event and then assert that the event was dispatched using Event::assertDispatched().

This approach ensures that your event subscriber is correctly listening to the event and that the appropriate method is called when the event is dispatched.

martinbean's avatar

@riseabove2_2 I create feature tests that test side effects to be honest.

So if a mailable needs to be sent when something happens, I’ll test that. I don’t care how it’s sent (listener, subscriber, etc) I just care that it was sent:

public function testEmailSentWhenApplicatedApproved(): void
{
    Mail::fake();

    $application = Application::factory()->create();

    $application->approve();

    Mail::assertQueued(ApplicationApproved::class);
}
1 like

Please or to participate in this conversation.