How to Write a unit test with laravel queue mail? - laravel

I already finish the code to sending the mail by queue, and it`s working fine,
and I want to write a test for it (just want to test the mail that it should be send out normally without failing by queue, and to the right person), but how?
Mail::to($user->email)->queue(new Welcome($user));

You would do something like this (depending on your setup):
<?php
namespace Tests\Feature;
use App\User;
use App\Mail\Welcome;
use Illuminate\Support\Facades\Mail;
class SendInvitationEmailTest extends TestCase
{
/** #test */
function mails_get_queued()
{
Mail::fake();
$user = factory(User::class)->create();
$this->post('/route/to/send/the/welcome/mail');
Mail::assertQueued(Welcome::class, 1);
Mail::assertQueued(Welcome::class, function ($mail) use ($user) {
return $mail->user->id === $user->id;
});
}
}

I would recommend grabbing the queued emails from Mail::queued. It's a simple array that then gives you all the power you want.
Like this:
$queuedEmails = Mail::queued(CustomerEmail::class);
$this->assertCount(1, $queuedEmails);
$email = $queuedEmails[0];
$this->assertEquals('status_complete', $email->handle);
You can run asserts as you're used to, which provide more meaningful messages in the case of failure. Unfortunately, Mail::assertQueued's failure report isn't very specific or helpful:
The expected [App\Mail\MyEmail] mailable was not queued.
Failed asserting that false is true.
That's if you simply return true or false in the callback version. Note that you can use asserts in the callback, which is great, it's just more awkward if you need to check more than one email.

Now I'm using the MailTracking to test the mail
https://gist.github.com/anonymous/6e802e56af1f19d53464d667b3e6aa48

Related

How to test a route in Laravel that uses both `Storage::put()` and `Storage::temporaryUrl()`?

I have a route in Laravel 7 that saves a file to a S3 disk and returns a temporary URL to it. Simplified the code looks like this:
Storage::disk('s3')->put('image.jpg', $file);
return Storage::disk('s3')->temporaryUrl('image.jpg');
I want to write a test for that route. This is normally straightforward with Laravel. I mock the storage with Storage::fake('s3') and assert the file creation with Storage::disk('s3')->assertExists('image.jpg').
The fake storage does not support Storage::temporaryUrl(). If trying to use that method it throws the following error:
This driver does not support creating temporary URLs.
A common work-a-round is to use Laravel's low level mocking API like this:
Storage::shouldReceive('temporaryUrl')
->once()
->andReturn('http://examples.com/a-temporary-url');
This solution is recommended in a LaraCasts thread and a GitHub issue about that limitation of Storage::fake().
Is there any way I can combine that two approaches to test a route that does both?
I would like to avoid reimplementing Storage::fake(). Also, I would like to avoid adding a check into the production code to not call Storage::temporaryUrl() if the environment is testing. The latter one is another work-a-round proposed in the LaraCasts thread already mentioned above.
I had the same problem and came up with the following solution:
$fakeFilesystem = Storage::fake('somediskname');
$proxyMockedFakeFilesystem = Mockery::mock($fakeFilesystem);
$proxyMockedFakeFilesystem->shouldReceive('temporaryUrl')
->andReturn('http://some-signed-url.test');
Storage::set('somediskname', $proxyMockedFakeFilesystem);
Now Storage::disk('somediskname')->temporaryUrl('somefile.png', now()->addMinutes(20)) returns http://some-signed-url.test and I can actually store files in the temporary filesystem that Storage::fake() provides without any further changes.
Re #abenevaut answer above, and the problems experienced in the comments - the call to Storage::disk() also needs mocking - something like:
Storage::fake('s3');
Storage::shouldReceive('disk')
->andReturn(
new class()
{
public function temporaryUrl($path)
{
return 'https://mock-aws.com/' . $path;
}
}
);
$expectedUrl = Storage::disk('s3')->temporaryUrl(
'some-path',
now()->addMinutes(5)
);
$this->assertEquals('https://mock-aws.com/some-path', $expectedUrl);
You can follow this article https://laravel-news.com/testing-file-uploads-with-laravel, and mix it with your needs like follow; Mocks seem cumulative:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class ExampleTest extends TestCase
{
public function testAvatarUpload()
{
$temporaryUrl = 'http://examples.com/a-temporary-url';
Storage::fake('avatars');
/*
* >>> Your Specific Asserts HERE
*/
Storage::shouldReceive('temporaryUrl')
->once()
->andReturn($temporaryUrl);
$response = $this->json('POST', '/avatar', [
'avatar' => UploadedFile::fake()->image('avatar.jpg')
]);
$this->assertContains($response, $temporaryUrl);
// Assert the file was stored...
Storage::disk('avatars')->assertExists('avatar.jpg');
// Assert a file does not exist...
Storage::disk('avatars')->assertMissing('missing.jpg');
}
}
Another exemple for console feature tests:
command : https://github.com/abenevaut/pokemon-friends.com/blob/1.1.3/app/Console/Commands/PushFileToAwsCommand.php
test : https://github.com/abenevaut/pokemon-friends.com/blob/1.1.6/tests/Feature/Console/Files/PushFileToCloudCommandTest.php

Laravel 5.6 testing Notification::assertSentTo() not found

Struggling since multiple days to get Notification::assertSentTo() method working in my feature test of reset password emails in a Laravel 5.6 app, yet receiving ongoing failures with following code:
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Support\Facades\Notification;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class UserPasswordResetTest extends TestCase
{
public function test_submit_password_reset_request()
{
$user = factory("App\User")->create();
$this->followingRedirects()
->from(route('password.request'))
->post(route('password.email'), [ "email" => $user->email ]);
Notification::assertSentTo($user, ResetPassword::class);
}
}
I have tried several ideas including to use Illuminate\Support\Testing\Fakes\NotificationFake directly in the use list.
In any attempt the tests keep failing with
Error: Call to undefined method Illuminate\Notifications\Channels\MailChannel::assertSentTo()
Looking forward to any hints helping towards a succesful test.
Regards & take care!
It seems like you are missing a Notification::fake(); For the correct fake notification driver to be used.
Notification::fake();
$this->followingRedirects()
...

Cannot get Laravel Mail::to()->later() to work

I'm trying to send a time-delayed email via a Redis queue and Mailgun using the following code...
$when = \Carbon\Carbon::now()->addMinutes(5);
Mail::to($demoDownloader->Email)->later($when, new DemoRequestFollowUp($demoDownloader));
I can see that the job is added to the redis queue (using Redis Desktop Manager) and stays in the queue for 5 minutes before disappearing from the queue. Unfortunately, it never appears in the Mailgun logs.
If I instead use this line of code...
Mail::to($demoDownloader->Email)->send(new DemoRequestFollowUp($demoDownloader));
...then the email appears in the Mailgun logs and subsequently arrives in the destination mailbox successfully (minus the time delay of course).
No error messages are written to storage/logs/laravel.log so I'm at a bit of a loss as to why this isn't working. I'm pretty sure I've used the syntax specified in the manual.
By the way, my mailable looks like this...
<?php
namespace App\Mail;
use \App\DemoDownloader;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class DemoRequestFollowUp extends Mailable
{
use Queueable, SerializesModels;
public $requestee;
public function __construct(DemoDownloader $requestee)
{
$this->requestee = $requestee;
}
public function build()
{
return $this->subject('Overview')
->view('email-demo-request-follow-up');
}
}
I'd be most grateful for any input.
I'd neglected to look in the failed_jobs table which gave me the clue I needed...
Illuminate\Database\Eloquent\ModelNotFoundException: No query results for model [App\DemoDownloader]
As the DemoDownloader instance being passed into the DemoRequestFollowUp constructor wasn't actually being used, I removed the argument and also removed the $requestee public property from the class and its assignment in the constructor. After making these changes emails were reliably arriving after the specified delay interval had passed.

Database notifications don't show up after testing notify

I have a unit test with the following:
use \Illuminate\Notifications\DatabaseNotification;
public function testMailSentAndLogged()
{
Notification::fake();
$user = factory(User::class)->create();
$emailAddress = $user->emailAddress;
$emailAddress->notify(new UserCreated);
Notification::assertSentTo(
$emailAddress,
UserCreated::class
);
error_log('DatabaseNotification '.print_r(DatabaseNotification::get()->toArray(), 1));
$this->assertEquals(1, $emailAddress->notifications->count());
}
My Notification has this for the via():
public final function via($notifiable)
{
// complex logic...
error_log('mail, database');
return ['mail', 'database'];
}
The code fails on the $this->assertEquals code. the error_log produces the following:
[03-Jan-2018 01:23:01 UTC] mail, database
[03-Jan-2018 01:23:01 UTC] DatabaseNotification Array
(
)
WHY don't the $emailAddress->notifications pull up anything? Why doesn't DatabaseNotification::get() pull anything?;
In your test, you are calling the method
Notification::fake();
As stated in Laravel's documentation on Mocking,
You may use the Notification facade's fake method to prevent
notifications from being sent.
Actually, this bit of code is the assertion that the Notification would have been sent, under normal circumstances (ie in prod) :
Notification::assertSentTo();
If you remove the call to Notification::fake(), your notification should appear in your testing database.
So you kinda have two solutions. The first one is to remove the call to fake(), thus really sending the notification, which will appear in the database. the second is not to test if the notification was written successfully in the database : it's Laravel's responsibility, not your application's. I recommand the second solution :)

How to test Mail Facade in Laravel 4

I cant seem to get the Mail Facade to accept a ->with() command for Testing.
This works:
Mail::shouldReceive('send')->once();
But this does not work:
Mail::shouldReceive('send')->with('emails.welcome')->once();
and neither does this:
Mail::shouldReceive('send')->with('emails.welcome', array(), function(){})->once();
and neither does this:
Mail::shouldReceive('send')->with('emails.welcome', array(), function($message){})->once();
All give the following error:
"No matching handler found for Illuminate\Mail\Mailer::send("emails.welcome", Array, Closure)"
So how can I test Mail to check what it is receiving?
Also - for bonus points - is it possible to test what Mail is doing inside the closure? i.e. can I check what $message->to() is set to?
edit: my mail code:
Mail::send("emails.welcome", $data, function($message)
{
$message->to($data['email'], $data['name'])->subject('Welcome!');
});
The code examples below assumes PHP 5.4 or newer - if you're on 5.3 you'll need to add $self = $this before the following code and use ($self) on the first closure, and replace all references to $this inside the closure.
Mocking SwiftMailer
The simplest way is to mock the Swift_Mailer instance. You'll have to read up on what methods exist on the Swift_Message class in order to take full advantage of it.
$mock = Mockery::mock('Swift_Mailer');
$this->app['mailer']->setSwiftMailer($mock);
$mock->shouldReceive('send')->once()
->andReturnUsing(function(\Swift_Message $msg) {
$this->assertEquals('My subject', $msg->getSubject());
$this->assertEquals('foo#bar.com', $msg->getTo());
$this->assertContains('Some string', $msg->getBody());
});
Assertions on closures
Another way to solve this is to run assertions on the closure passed to Mail::send. This does not look all that clean, and its error messages can be rather cryptic, but it works, is very flexible, and the technique can be used for other things as well.
use Mockery as m;
Mail::shouldReceive('send')->once()
->with('view.name', m::on(function($data) {
$this->assertContains('my variable', $data);
return true;
}), m::on(function($closure) {
$message = m::mock('Illuminate\Mailer\Message');
$message->shouldReceive('to')
->with('test#example.com')
->andReturn(m::self());
$message->shouldReceive('subject')
->with('Email subject')
->andReturn(m::self());
$closure($message);
return true;
}));
In this example, I'm running an assertion on the data passed to the view, and I'll get an error from Mockery if the recipient address, subject or view name is wrong.
Mockery::on() allows you to run a closure on a parameter of a mocked method. If it returns false, you'll get the "No matching handler found", but we want to run assertions so we just return true. Mockery::self() allows for chaining of methods.
If at any point you don't care what a certain parameter of a method call is, you can use Mockery::any() to tell Mockery that it accepts anything.

Resources