简体   繁体   中英

Laravel unit testing emails

My system sends a couple of important emails. What is the best way to unit test that?

I see you can put it in pretend mode and it goes in the log. Is there something to check that?

There are two options.

Option 1 - Mock the mail facade to test the mail is being sent. Something like this would work:

$mock = Mockery::mock('Swift_Mailer');
$this->app['mailer']->setSwiftMailer($mock);
$mock->shouldReceive('send')->once()
    ->andReturnUsing(function($msg) {
        $this->assertEquals('My subject', $msg->getSubject());
        $this->assertEquals('foo@bar.com', $msg->getTo());
        $this->assertContains('Some string', $msg->getBody());
    });

Option 2 is much easier - it is to test the actual SMTP using MailCatcher.me . Basically you can send SMTP emails, and 'test' the email that is actually sent. Laracasts has a great lesson on how to use it as part of your Laravel testing here .

对于 Laravel 5.4 检查Mail::fake()https ://laravel.com/docs/5.4/mocking#mail-fake

"Option 1" from "@The Shift Exchange" is not working in Laravel 5.1, so here is modified version using Proxied Partial Mock:

$mock = \Mockery::mock($this->app['mailer']->getSwiftMailer());
$this->app['mailer']->setSwiftMailer($mock);
$mock
    ->shouldReceive('send')
    ->withArgs([\Mockery::on(function($message)
    {
        $this->assertEquals('My subject', $message->getSubject());
        $this->assertSame(['foo@bar.com' => null], $message->getTo());
        $this->assertContains('Some string', $message->getBody());
        return true;
    }), \Mockery::any()])
    ->once();

If you just don't want the e-mails be really send, you can turn off them using the "Mail::pretend(true)"

class TestCase extends Illuminate\Foundation\Testing\TestCase {
    private function prepareForTests() {
      // e-mail will look like will be send but it is just pretending
      Mail::pretend(true);
      // if you want to test the routes
      Route::enableFilters();
    }
}

class MyTest extends TestCase {
    public function testEmail() {
      // be happy
    }
}

If any one is using docker as there development environment I end up solving this by:

Setup

.env

...
MAIL_FROM       = noreply@example.com

MAIL_DRIVER     = smtp
MAIL_HOST       = mail
EMAIL_PORT      = 1025
MAIL_URL_PORT   = 1080
MAIL_USERNAME   = null
MAIL_PASSWORD   = null
MAIL_ENCRYPTION = null

config/mail.php

# update ...

'port' => env('MAIL_PORT', 587),

# to ...

'port' => env('EMAIL_PORT', 587),

(I had a conflict with this environment variable for some reason)

Carrying on...

docker-compose.ymal

mail:
    image: schickling/mailcatcher
    ports:
        - 1080:1080

app/Http/Controllers/SomeController.php

use App\Mail\SomeMail;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;


class SomeController extends BaseController
{
    ...
    public function getSomething(Request $request)
    {
        ...
        Mail::to('someone@example.com')->send(new SomeMail('Body of the email'));
        ...
    }

app/Mail/SomeMail.php

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class SomeMail extends Mailable
{
    use Queueable, SerializesModels;

    public $body;

    public function __construct($body = 'Default message')
    {
        $this->body = $body;
    }

    public function build()
    {
        return $this
            ->from(ENV('MAIL_FROM'))
            ->subject('Some Subject')
            ->view('mail.someMail');
    }
}

resources/views/mail/SomeMail.blade.php

<h1>{{ $body }}</h1>

Testing

tests\\Feature\\EmailTest.php

use Tests\TestCase;
use Illuminate\Http\Request;
use App\Http\Controllers\SomeController;

class EmailTest extends TestCase
{
    privete $someController;
    private $requestMock;

    public function setUp()
    {
        $this->someController = new SomeController();
        $this->requestMock = \Mockery::mock(Request::class);
    }

    public function testEmailGetsSentSuccess()
    {
        $this->deleteAllEmailMessages();

        $emails = app()->make('swift.transport')->driver()->messages();
        $this->assertEmpty($emails);

        $response = $this->someController->getSomething($this->requestMock);

        $emails = app()->make('swift.transport')->driver()->messages();
        $this->assertNotEmpty($emails);

        $this->assertContains('Some Subject', $emails[0]->getSubject());
        $this->assertEquals('someone@example.com', array_keys($emails[0]->getTo())[0]);
    }

    ...

    private function deleteAllEmailMessages()
    {
        $mailcatcher = new Client(['base_uri' => config('mailtester.url')]);
        $mailcatcher->delete('/messages');
    }
}

(This has been copied and edited from my own code so might not work first time)

(source: https://stackoverflow.com/a/52177526/563247 )

I think that inspecting the log is not the good way to go.

You may want to take a look at how you can mock the Mail facade and check that it receives a call with some parameters.

if you are using Notifcations in laravel you can do that like below

Notification::fake();
$this->post(...);
$user = User::first();
Notification::assertSentTo([$user], VerifyEmail::class);

https://laravel.com/docs/7.x/mocking#notification-fake

If you want to test everything around the email, use

Mail::fake()

But if you want to test your Illuminate\\Mail\\Mailable and the blade , then follow this example. Say, you want to test a Reminder email about some payment, where the email text should have product called 'valorant' and some price in 'USD'.

 public function test_PaymentReminder(): void
{
    /* @var $payment SalePayment */
    $payment = factory(SalePayment::class)->create();
    auth()->logout();

    $paymentReminder = new PaymentReminder($payment);
    $html            = $paymentReminder->render();

    $this->assertTrue(strpos($html, 'valorant') !== false);
    $this->assertTrue(strpos($html, 'USD') !== false);
}

The important part here is ->render() - that is how you make Illuminate\\Mail\\Mailable to run build() function and process the blade .

Another importan thing is auth()->logout(); - because normally emails being processed in a queue that run in a background environment. This environment has no user and has no request with no URL and no IP...

So you must be sure that you are rendering the email in your unit test in a similar environment as in production.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM