Skip to content
Advertisement

Laravel Notification Mailable not working in Test

I’am using a notification to send a mail to all users when the notification gets Triggered.

This works fine in manual “browsing the website” usage (submit a POST to the ContactController) but in my tests the mail facade claims that no mail gets sent.

Controller:

<?php

namespace AppHttpControllers;

use AppNotificationsContactRequestNotification;
use AppUser;
use IlluminateHttpRequest;
use Notification;

class ContactController extends Controller
{
    public function create(Request $request)
    {
        $validateData = $request->validate([
            'name' => 'required',
            'email' => 'required|email',
            'message' => 'required'
        ]);

        Notification::send(User::all(), new ContactRequestNotification($validateData));

        return $validateData;
    }
}

Notification:

<?php

namespace AppNotifications;

use AppMailContactMail;
use IlluminateMailMailable;
use IlluminateNotificationsNotification;

class ContactRequestNotification extends Notification
{
    /**
     * @var array
     */
    private $contactData;

    /**
     * Create a new notification instance.
     *
     * @return void
     */
    public function __construct($contactData)
    {
        $this->contactData = $contactData;
    }

    /**
     * Get the notification's delivery channels.
     *
     * @param mixed $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return ['mail'];
    }

    /**
     * Get the mail representation of the notification.
     *
     * @param mixed $notifiable
     * @return Mailable
     */
    public function toMail($notifiable)
    {
        return (new ContactMail($this->contactData['name'], $this->contactData['email'], $this->contactData['message']))->to($notifiable->email);
    }

    /**
     * Get the array representation of the notification.
     *
     * @param mixed $notifiable
     * @return array
     */
    public function toArray($notifiable)
    {
        return $this->contactData;
    }
}

Mailable:

<?php

namespace AppMail;

use IlluminateBusQueueable;
use IlluminateMailMailable;
use IlluminateQueueSerializesModels;

class ContactMail extends Mailable
{
    use Queueable, SerializesModels;

    /** @var string */
    public $name;

    /** @var string */
    public $email;

    /** @var string */
    public $message;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct($name, $email, $message)
    {
        $this->name = $name;
        $this->email = $email;
        $this->message = $message;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->markdown('emails.contact')
            ->from(env('MAIL_FROM'))
            ->subject('New contact request');
    }
}

Test:

<?php

namespace TestsFeature;

use AppMailContactMail;
use AppNotificationsContactRequestNotification;
use AppUser;
use IlluminateFoundationTestingDatabaseMigrations;
use IlluminateSupportFacadesMail;
use Notification;
use TestsTestCase;

class ContactFormTest extends TestCase
{
    use DatabaseMigrations;

    /** @test */
    public function a_notification_gets_created_when_the_contact_form_is_used()
    {
        Notification::fake();

        $user = factory(User::class)->create();

        $response = $this->post('/contact', [
            'name' => 'John Doe',
            'email' => 'email@email.com',
            'message' => 'This is a test message'
        ]);

        $response->assertStatus(200);

        Notification::assertSentTo($user, ContactRequestNotification::class);
    }

    /** @test */
    public function a_mail_is_send_when_the_contact_form_is_used()
    {
        $this->withExceptionHandling();

        factory(User::class)->create();

        Mail::fake();

        Mail::assertNothingSent();

        $data = [
            'name' => 'John Doe',
            'email' => 'email@email.com',
            'message' => 'This is a test message'
        ];

        $response = $this->post('/contact', $data);

        $response->assertStatus(200);

        Mail::assertSent(ContactMail::class, 1);
    }
}

The “a_notification_gets_created_when_the_contact_form_is_used” test runs without any problems but the second test case results in:

/usr/bin/php /home/vendor/phpunit/phpunit/phpunit --configuration /home/phpunit.xml --filter "/(::a_mail_is_send_when_the_contact_form_is_used)( .*)?$/" TestsFeatureContactFormTest /home/tests/Feature/ContactFormTest.php --teamcity
PHPUnit 8.3.4 by Sebastian Bergmann and contributors.


The expected [AppMailContactMail] mailable was sent 0 times instead of 1 times.
Failed asserting that false is true.

When performing the same tasks in the browser a mail gets sent to the user.

Any tips on what i am missing here? An internet search revealed that some other people had this problem but neither found a solution (or that I am not capable of performing the correct search)

Thanks!

Advertisement

Answer

It looks like the MailChannel driver that sends notification emails doesn’t use the Mail facade which means Mail::fake wouldn’t affect it. Instead it calls the send method on the Mailable directly which in turns calls send on the Mailer (mail driver).

You could have replaced the Mailable instance with an instance of MailFake (which is what Mail::fake uses), but it looks like MailFake doesn’t cater for the case when the $view is an array (which is what the MailChannel passes to Mailable).

Luckily the Laravel source contains an example of how they test notifications that send mails in the SendingMailNotificationsTest. They mock the Mailer and Markdown instances and check the parameters that get passed. You could do something similar like:

use Mockery as m;
use IlluminateContractsMailMailable;
use IlluminateContractsMailMailer;
use IlluminateMailMarkdown;
use IlluminateMailMessage;

class ContactFormTest extends TestCase
{

    protected function tearDown(): void
    {
        parent::tearDown();
        m::close();
    }

    protected function setUp(): void
    {
        parent::setUp();

        $this->mailer = m::mock(Mailer::class);
        $this->markdown = m::mock(Markdown::class);
        $this->instance(Mailer::class, $this->mailer);
        $this->instance(Mailer::class, $this->markdown);
    }

    public function a_mail_is_send_when_the_contact_form_is_used()
    {
        $this->withExceptionHandling();

        $user = factory(User::class)->create();

        $this->markdown->shouldReceive('render')->once()->andReturn('htmlContent');

        $this->markdown->shouldReceive('renderText')->once()->andReturn('textContent');

        $data = [
            'name' => 'John Doe',
            'email' => 'email@email.com',
            'message' => 'This is a test message'
        ];

        $notification = new ContactRequestNotification($data);

        $this->mailer->shouldReceive('send')->once()->with(
            ['html' => 'htmlContent', 'text' => 'textContent'],
            array_merge($notification->toMail($user)->toArray(), [
                '__laravel_notification' => get_class($notification),
                '__laravel_notification_queued' => false,
            ]),
            m::on(function ($closure) {
                $message = m::mock(Message::class);
                $message->shouldReceive('to')->once()->with([$user->email]);
                $closure($message);
                return true;
            })
        );


        $response = $this->post('/contact', $data);

        $response->assertStatus(200);

    }
}

Personally, I would rather just unit test the toMail method on the ContactRequestNotification class for now because I don’t think the method above is very pretty.

User contributions licensed under: CC BY-SA
9 People found this is helpful
Advertisement