Skip to content

Commit

Permalink
Merge pull request #13 from kbond/collection
Browse files Browse the repository at this point in the history
SentEmails collection improvements
  • Loading branch information
kbond committed Oct 8, 2021
2 parents 379624a + f64e7f2 commit db31012
Show file tree
Hide file tree
Showing 8 changed files with 360 additions and 121 deletions.
84 changes: 81 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class MyTest extends KernelTestCase // or WebTestCase
$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')
Expand Down Expand Up @@ -73,6 +74,74 @@ class MyTest extends KernelTestCase // or WebTestCase
**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.

```php
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`
Expand Down Expand Up @@ -105,19 +174,26 @@ use Zenstruck\Mailer\Test\InteractsWithMailer;

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

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

$this->mailer()->sentTestEmails(AppTestEmail::class); // AppTestEmail[]

// 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[]
}
}
```
Expand Down Expand Up @@ -159,6 +235,8 @@ $browser
// see Usage section above for full API
})
;

$component->sentEmails(); \Zenstruck\Mailer\Test\SentEmails
})
;
```
Expand Down
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
"php": ">=7.4",
"symfony/framework-bundle": "^4.4|^5.0",
"symfony/mailer": "^4.4|^5.0",
"symfony/polyfill-php80": "^1.23.1",
"zenstruck/assert": "^1.0",
"zenstruck/callback": "^1.1"
},
"require-dev": {
"phpunit/phpunit": "^9.5.0",
"symfony/messenger": "^4.4|^5.0",
"symfony/phpunit-bridge": "^5.2",
"symfony/var-dumper": "^4.4|^5.0",
"symfony/yaml": "^4.4|^5.0",
"zenstruck/browser": "^1.0@dev"
},
Expand Down
2 changes: 1 addition & 1 deletion src/Bridge/Zenstruck/Browser/MailerExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ trait MailerExtension
{
use SentEmailMixin;

public function sentEmails(): SentEmails
private function sentEmails(): SentEmails
{
return (new MailerComponent($this))->sentEmails();
}
Expand Down
38 changes: 10 additions & 28 deletions src/SentEmailMixin.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@

namespace Zenstruck\Mailer\Test;

use Symfony\Component\Mime\Address;
use Zenstruck\Assert;
use Zenstruck\Callback;
use Zenstruck\Callback\Parameter;

/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
Expand All @@ -33,39 +28,26 @@ public function assertSentEmailCount(int $expected): self
}

/**
* @param callable|string $callback Takes an instance of the found Email as TestEmail - if string, assume subject
* @param callable|string $callback callable: {@see TestEmail::call()}
* string: subject
*
* @return static
*/
public function assertEmailSentTo(string $expectedTo, $callback): self
{
$emails = $this->sentEmails();

if (0 === \count($emails)) {
Assert::fail('No emails have been sent.');
}

if (!\is_callable($callback)) {
$callback = static fn(TestEmail $message) => $message->assertSubject($callback);
}

$foundToAddresses = [];

foreach ($emails as $email) {
$toAddresses = \array_map(static fn(Address $address) => $address->getAddress(), $email->getTo());
$foundToAddresses[] = $toAddresses;
$this->sentEmails()
->ensureSome('No emails have been sent.')
->whereTo($expectedTo)
->ensureSome('No email was sent to "{expected}".', ['expected' => $expectedTo])
->first()
->call($callback)
;

if (\in_array($expectedTo, $toAddresses, true)) {
// address matches
Callback::createFor($callback)->invoke(
Parameter::typed(TestEmail::class, Parameter::factory(fn(string $class) => new $class($email)))
);

return $this;
}
}

Assert::fail(\sprintf('Email sent, but "%s" is not among to-addresses: %s', $expectedTo, \implode(', ', \array_merge(...$foundToAddresses))));
return $this;
}

abstract public function sentEmails(): SentEmails;
Expand Down
Loading

0 comments on commit db31012

Please sign in to comment.