zenstruck/mailer-test

Alternative, opinionated helpers for testing emails sent with symfony/mailer.

Fund package maintenance!
kbond

v1.4.1 2023-10-25 13:45 UTC

This package is auto-updated.

Last update: 2024-04-09 18:30:58 UTC


README

CI Status Code Coverage

Alternative, opinionated helpers for testing emails sent with symfony/mailer. This package is an alternative to the FrameworkBundle's MailerAssertionsTrait.

Installation

  1. Install the library:
    composer require --dev zenstruck/mailer-test
  2. If not added automatically by symfony/flex, enable ZenstruckMailerTestBundle in your test environment

Usage

You can interact with the mailer in your tests by using the InteractsWithMailer trait in your KernelTestCase/WebTestCase tests:

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Zenstruck\Mailer\Test\InteractsWithMailer;
use Zenstruck\Mailer\Test\TestEmail;

class MyTest extends KernelTestCase // or WebTestCase
{
    use InteractsWithMailer;

    public function test_something(): void
    {
        // ...some code that sends emails...

        $this->mailer()->assertNoEmailSent();
        $this->mailer()->assertSentEmailCount(5);
        $this->mailer()->assertEmailSentTo('kevin@example.com', 'the subject');

        // For more advanced assertions, use a callback for the subject.
        // Note the \Zenstruck\Mailer\Test\TestEmail argument. This is a decorator
        // around \Symfony\Component\Mime\Email with some extra assertions.
        $this->mailer()->assertEmailSentTo('kevin@example.com', function(TestEmail $email) {
            $email
                ->assertSubject('Email Subject')
                ->assertSubjectContains('Subject')
                ->assertFrom('from@example.com')
                ->assertReplyTo('reply@example.com')
                ->assertCc('cc1@example.com')
                ->assertCc('cc2@example.com')
                ->assertBcc('bcc@example.com')
                ->assertTextContains('some text')
                ->assertHtmlContains('some text')
                ->assertContains('some text') // asserts text and html both contain a value
                ->assertHasFile('file.txt', 'text/plain', 'Hello there!')

                // tag/meta data assertions (https://symfony.com/doc/current/mailer.html#adding-tags-and-metadata-to-emails)
                ->assertHasTag('password-reset')
                ->assertHasMetadata('Color')
                ->assertHasMetadata('Color', 'blue')
            ;

            // Any \Symfony\Component\Mime\Email methods can be used
            $this->assertSame('value', $email->getHeaders()->get('X-SOME-HEADER')->getBodyAsString());
        });

        // reset collected emails
        $this->mailer()->reset();
    }
}

NOTE: Emails are persisted between kernel reboots within each test. You can reset the collected emails with $this->mailer()->reset().

SentEmails Collection

You can access all the sent emails and filter down to just the ones you want to make assertions on. Most methods are fluent.

use Symfony\Component\Mime\Email;
use Zenstruck\Mailer\Test\SentEmails;
use Zenstruck\Mailer\Test\TestEmail;

/** @var SentEmails $sentEmails */
$sentEmails = $this->mailer()->sentEmails();

$sentEmails->all(); // TestEmail[]
$sentEmails->raw(); // Email[]

$sentEmails->first(); // First TestEmail in collection or fail if none
$sentEmails->last(); // Last TestEmail in collection or fail
$sentEmails->count(); // # of emails in collection
$sentEmails->dump(); // dump() the collection
$sentEmails->dd(); // dd() the collection

$sentEmails->each(function(TestEmail $email) {
    // do something with each email in collection
});
$sentEmails->each(function(Email $email) {
    // can typehint as Email
});

// iterate over collection
foreach ($sentEmails as $email) {
    /** @var TestEmail $email */
}

// assertions
$sentEmails->assertNone();
$sentEmails->assertCount(5);

// fails if collection is empty
$sentEmails->ensureSome();
$sentEmails->ensureSome('custom failure message');

// filters - returns new instance of SentEmails
$sentEmails->whereSubject('some subject'); // emails with subject "some subject"
$sentEmails->whereSubjectContains('subject'); // emails where subject contains "subject"
$sentEmails->whereFrom('sally@example.com'); // emails sent from "sally@example.com"
$sentEmails->whereTo('sally@example.com'); // emails sent to "sally@example.com"
$sentEmails->whereCc('sally@example.com'); // emails cc'd to "sally@example.com"
$sentEmails->whereBcc('sally@example.com'); // emails bcc'd to "sally@example.com"
$sentEmails->whereReplyTo('sally@example.com'); // emails with "sally@example.com" as a reply-to
$sentEmails->whereTag('password-reset'); // emails with "password-reset" tag (https://symfony.com/doc/current/mailer.html#adding-tags-and-metadata-to-emails)

// custom filter
$sentEmails->where(function(TestEmail $email): bool {
    return 'password-reset' === $email->tag() && 'Some subject' === $email->getSubject();
});

// combine filters
$sentEmails
    ->whereTag('password-reset')
    ->assertCount(2)
    ->each(function(TestEmail $email) {
        $email->assertSubjectContains('Password Reset');
    })
    ->whereTo('kevin@example.com')
    ->assertCount(1)

Custom TestEmail

The TestEmail class shown above is a decorator for \Symfony\Component\Mime\Email with some assertions. You can extend this to add your own assertions:

namespace App\Tests;

use PHPUnit\Framework\Assert;
use Zenstruck\Mailer\Test\TestEmail;

class AppTestEmail extends TestEmail
{
    public function assertHasPostmarkTag(string $expected): self
    {
        Assert::assertTrue($this->getHeaders()->has('X-PM-Tag'));
        Assert::assertSame($expected, $this->getHeaders()->get('X-PM-Tag')->getBodyAsString());

        return $this;
    }
}

Then, use in your tests:

use App\Tests\AppTestEmail;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Zenstruck\Mailer\Test\InteractsWithMailer;

class MyTest extends KernelTestCase // or WebTestCase
{
    use InteractsWithMailer;

    public function test_something(): void
    {
        // ...some code that sends emails...

        // Type-hinting the callback with your custom TestEmail triggers it to be
        // injected instead of the standard TestEmail.
        $this->mailer()->assertEmailSentTo('kevin@example.com', function(AppTestEmail $email) {
            $email->assertHasPostmarkTag('password-reset');
        });

        $this->mailer()->sentEmails()->each(function(AppTestEmail $email) {
            $email->assertHasPostmarkTag('password-reset');
        });

        // add your custom TestEmail as an argument to these methods to change the return type
        $this->mailer()->sentEmails()->first(AppTestEmail::class); // AppTestEmail
        $this->mailer()->sentEmails()->last(AppTestEmail::class); // AppTestEmail
        $this->mailer()->sentEmails()->all(AppTestEmail::class); // AppTestEmail[]
    }
}

zenstruck/browser Integration

This library provides a zenstruck/browser "Component" and "Extension". Since browser's make HTTP requests to your app, the messages are accessed via the profiler (using symfony/mailer's data collector). Because of this, the InteractsWithMailer trait is not required in your test case. Since the profiler is required, this functionality is not available with PantherBrowser.

MailerComponent

The simplest way to get started testing emails with zenstruck/browser is to use the MailerComponent:

use Zenstruck\Mailer\Test\Bridge\Zenstruck\Browser\MailerComponent;
use Zenstruck\Mailer\Test\TestEmail;

/** @var \Zenstruck\Browser\KernelBrowser $browser **/
$browser
    ->withProfiling() // enable the profiler for the next request
    ->visit('/page/that/does/not/send/email')
    ->use(function(MailerComponent $component) {
        $component->assertNoEmailSent();
    })

    ->withProfiling() // enable the profiler for the next request
    ->visit('/page/that/sends/email')
    ->use(function(MailerComponent $component) {
        $component
            ->assertSentEmailCount(1)
            ->assertEmailSentTo('kevin@example.com', 'Email Subject')
            ->assertEmailSentTo('kevin@example.com', function(TestEmail $email) {
                // see Usage section above for full API
            })
        ;

        $component->sentEmails(); \Zenstruck\Mailer\Test\SentEmails
    })
;

MailerExtension

If many of your tests make email assertions the MailerComponent's API can be a little verbose. Alternatively, you can add the methods directly on a custom browser using the provided MailerExtension trait:

namespace App\Tests;

use Zenstruck\Browser\KernelBrowser;
use Zenstruck\Mailer\Test\Bridge\Zenstruck\Browser\MailerExtension;

class AppBrowser extends KernelBrowser
{
    use MailerExtension;
}

Now, within your tests using this custom browser, the following email assertion API is available:

use Zenstruck\Mailer\Test\TestEmail;

/** @var \App\Tests\AppBrowser $browser **/
$browser
    ->withProfiling() // enable the profiler for the next request
    ->visit('/page/that/does/not/send/email')
    ->assertNoEmailSent()

    ->withProfiling() // enable the profiler for the next request
    ->visit('/page/that/sends/email')
    ->assertSentEmailCount(1)
    ->assertEmailSentTo('kevin@example.com', 'Email Subject')
    ->assertEmailSentTo('kevin@example.com', function(TestEmail $email) {
        // see Usage section above for full API
    })
;