This course is still being released! Check back later for more chapters.

Get Notified About this Course!

We will send you messages regarding this course only
and nothing else, we promise.
You can unsubscribe anytime by emailing us at:
privacy@symfonycasts.com
Login to bookmark this video
14.

Emails Assertions in Functional Tests

|

Share this awesome video!

|

Okay, testing time! If you've explored the codebase a bit, you may have noticed that someone (it could've been anyone... but probably a Canadian) snuck some tests into our tests/Functional/ directory. Do these pass? Idk! Let's find out!

Jump over to your terminal and run:

bin/phpunit

Uh-oh, 1 failure. Uh-oh, because, truth time, I'm the friendly Canadian that added these and I know they were passing at the beginning of the course! The failure is in BookingTest, specifically, testCreateBooking:

Expected redirect status code but got 500

on line 38 of BookingTest. That's where we send the email... so if we're looking for someone to blame, I feel like we should start with the Canadian, ahem, me and my wild email-sending ways.

Foundry and Browser

Open BookingTest.php. If you've written functional tests with Symfony before, this may look a tad different because I'm using some rocking helper libraries. zenstruck/foundry gives us this ResetDatabase trait which wipes the database before each test. It also gives us this Factories trait which lets us create database fixtures in our tests. And HasBrowser is from another package - zenstruck/browser - and is essentially a user-friendly wrapper around Symfony's test client.

testCreateBooking is the actual test. First, we create a Trip in the database with these known values. Next, some pre-assertions to ensure there are no bookings or customers in the database. Now, we use ->browser() to navigate to a trip page, fill in the booking form, and submit. We then assert that we're redirected to a specific booking URL and check that the page contains some expected HTML. Finally, we use Foundry to make some assertions about the data in our database.

->throwExceptions()

Line 38 caused the failure... we're getting a 500 response code when redirecting to this booking page. 500 status codes in tests can be frustrating because it can be hard to track down the actual exception. Luckily, Browser allows us to throw the actual exception. At the beginning of this chain, add ->throwExceptions():

55 lines | tests/Functional/BookingTest.php
// ... lines 1 - 12
class BookingTest extends KernelTestCase
{
// ... lines 15 - 19
public function testCreateBooking(): void
{
// ... lines 22 - 30
$this->browser()
->throwExceptions()
// ... lines 33 - 42
;
// ... lines 44 - 52
}
}

Back in the terminal, run the tests again:

bin/phpunit

Now we see an exception: Unable to find template "@images/mars.png". If you recall, this looks like how we're embedding the trip images into our email. It's failing because mars.png doesn't exist in public/imgs. For simplicity, let's adjust our test to use an existing image. For our fixture here, change mars to iss, and down below, for ->visit(): /trip/iss:

55 lines | tests/Functional/BookingTest.php
// ... lines 1 - 12
class BookingTest extends KernelTestCase
{
// ... lines 15 - 19
public function testCreateBooking(): void
{
$trip = TripFactory::createOne([
// ... line 23
'slug' => 'iss',
// ... line 25
]);
// ... lines 27 - 30
$this->browser()
// ... line 32
->visit('/trip/iss')
// ... lines 34 - 42
;
// ... lines 44 - 52
}
}

Run the tests again!

bin/phpunit

Passing!

It looks like our email is being sent... but let's confirm! At the end of this test, I want to make some email assertions. Symfony does allow this out of the box, but I like to use a library that puts the fun back in email functional testing.

zenstruck/mailer-test

At your terminal, run:

composer require --dev zenstruck/mailer-test

Installed and configured... back in our test, enable it by adding the InteractsWithMailer trait:

56 lines | tests/Functional/BookingTest.php
// ... lines 1 - 13
class BookingTest extends KernelTestCase
{
use ResetDatabase, Factories, HasBrowser, InteractsWithMailer;
// ... lines 17 - 54
}

Start simple, at the end of the test, write $this->mailer()->assertSentEmailCount(1);:

60 lines | tests/Functional/BookingTest.php
// ... lines 1 - 13
class BookingTest extends KernelTestCase
{
// ... lines 16 - 20
public function testCreateBooking(): void
{
// ... lines 23 - 54
$this->mailer()
->assertSentEmailCount(1)
;
}
}

Test-specific Environment Variables

Quick note: .env.local - where we put our real Mailtrap credentials - is not read or used in the test environment: our tests only load .env and this .env.test file. And in .env, MAILER_DSN is set to null://null. That's great! We want our tests to be fast, and not actually sending emails.

Re-run them!

bin/phpunit

assertEmailSentTo()

Passing - 1 email is being sent! Go back and add another assertion: ->assertEmailSentTo(). What email address are we expecting? The one we filled in the form: bruce@wayne-enterprises.com. Copy and paste that. The second argument is the subject: Booking Confirmation for Visit Mars:

61 lines | tests/Functional/BookingTest.php
// ... lines 1 - 13
class BookingTest extends KernelTestCase
{
// ... lines 16 - 20
public function testCreateBooking(): void
{
// ... lines 23 - 54
$this->mailer()
// ... line 56
->assertEmailSentTo('bruce@wayne-enterprises.com', 'Booking Confirmation for Visit Mars')
;
}
}

Run the tests!

bin/phpunit

Still passing! And notice we have 20 assertions now instead of 19.

TestEmail

But we can go further! Instead of a string for the subject in this assertion, use a closure with TestEmail $email as the argument:

69 lines | tests/Functional/BookingTest.php
// ... lines 1 - 14
class BookingTest extends KernelTestCase
{
// ... lines 17 - 21
public function testCreateBooking(): void
{
// ... lines 24 - 55
$this->mailer()
// ... line 57
->assertEmailSentTo('bruce@wayne-enterprises.com', function(TestEmail $email) {
// ... lines 59 - 64
})
;
}
}

Inside, we can now make loads more assertions on this email. Since we aren't checking the subject above anymore, add this one first: $email->assertSubject('Booking Confirmation for Visit Mars'):

69 lines | tests/Functional/BookingTest.php
// ... lines 1 - 14
class BookingTest extends KernelTestCase
{
// ... lines 17 - 21
public function testCreateBooking(): void
{
// ... lines 24 - 55
$this->mailer()
// ... line 57
->assertEmailSentTo('bruce@wayne-enterprises.com', function(TestEmail $email) {
$email
->assertSubject('Booking Confirmation for Visit Mars')
// ... lines 61 - 63
;
})
;
}
}

And we can chain more assertions!

Write ->assert to see what our editor suggests. Look at them all... Note the assertTextContains and assertHtmlContains. You can assert on each of these separately, but, because it's a best practice for both to contain the important details, use assertContains() to check both at once. Check for Visit Mars:

69 lines | tests/Functional/BookingTest.php
// ... lines 1 - 14
class BookingTest extends KernelTestCase
{
// ... lines 17 - 21
public function testCreateBooking(): void
{
// ... lines 24 - 55
$this->mailer()
// ... line 57
->assertEmailSentTo('bruce@wayne-enterprises.com', function(TestEmail $email) {
$email
// ... line 60
->assertContains('Visit Mars')
// ... lines 62 - 63
;
})
;
}
}

Links are important to check, so make sure the booking URL is there: ->assertContains('/booking/'.. Now, BookingFactory::first()->getUid():

69 lines | tests/Functional/BookingTest.php
// ... lines 1 - 14
class BookingTest extends KernelTestCase
{
// ... lines 17 - 21
public function testCreateBooking(): void
{
// ... lines 24 - 55
$this->mailer()
// ... line 57
->assertEmailSentTo('bruce@wayne-enterprises.com', function(TestEmail $email) {
$email
// ... lines 60 - 61
->assertContains('/booking/'.BookingFactory::first()->getUid())
// ... line 63
;
})
;
}
}

this fetches the first Booking entity in the database (which we know from above there is only the one), and gets its uid.

Heck! We can even check the attachment: ->assertHasFile('Terms of Service.pdf'):

69 lines | tests/Functional/BookingTest.php
// ... lines 1 - 14
class BookingTest extends KernelTestCase
{
// ... lines 17 - 21
public function testCreateBooking(): void
{
// ... lines 24 - 55
$this->mailer()
// ... line 57
->assertEmailSentTo('bruce@wayne-enterprises.com', function(TestEmail $email) {
$email
// ... lines 60 - 62
->assertHasFile('Terms of Service.pdf')
;
})
;
}
}

You can check the content-type and file contents via extra arguments, but I'm fine just checking that the attachment exists for now.

Go tests go!

bin/phpunit

Awesome, 25 assertions now!

->dd()

One last thing: if you're ever having trouble figuring out why one of these email assertions isn't passing, chain a ->dd():

70 lines | tests/Functional/BookingTest.php
// ... lines 1 - 14
class BookingTest extends KernelTestCase
{
// ... lines 17 - 21
public function testCreateBooking(): void
{
// ... lines 24 - 55
$this->mailer()
// ... line 57
->assertEmailSentTo('bruce@wayne-enterprises.com', function(TestEmail $email) {
$email
// ... lines 60 - 63
->dd()
;
})
;
}
}

and run your tests. When it hits that dd(), it dumps the email to help you debug. Don't forget to remove it when you're done!

Next, I want to add a second email to our app. To avoid duplication and keep things consistent, we'll create a Twig email layout that both share.