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.