dcsimg
December 2, 2016
Hot Topics:

An Introduction to MailThief in Laravel

By Terry Rowland

After attending Laracon 2016 this past week and hearing Adam Wathan's talk on TDD (Test Driven Development), where he used a package his company had developed and open-sourced, called "MailThief", I couldn't help but recall how much of a pain it was to test email. So, after seeing this, my eyes lit up and I wanted to see just how easy this could be!

As a disclaimer, this article assumes you are familiar with PHPUnit, Laravel, and the "Mail" façade in Laravel. Additionally, this article is going to assume you have a base Laravel application installed. If you don't, follow the install guide and then come back once that's done.

Here is the link to the repository where you can grab the code for the end product.

Okay, so now that we have a fresh Laravel install, we are going to need to pull in MailThief. There are two ways to install it, easily both via composer. From the application's root directory, run "composer require tightenco/MailThief --dev" from the console. It will install the package and in the "require-dev" section of your composer file, which is exactly where we want it because we would only use this in a testing environment. Alternatively, you could put "tightenco/MailThief": "^0.2.2" in the "require-dive" of your composer file and run "composer update" from the application's root folder and it will install it.

So, with MailThief installed, we now can get to using it. I'm not going to use a database in an effort to remove that complexity. Instead, I'm going to do a simple route (to a closure) and write a test to verify it works like I'm wanting it to.

Opening up the tests\ExampleTest.php that Laravel provides you will serve as an easy entry point. We will start with clearing out the testBasicExample function and making it look like this:

   public function testBasicExample()
   {

   }

Now, we can start getting our test setup at a very basic level.

First, we want to import the MailThief class by adding the following above the "class ExampleTest extends TestCase" declaration. Your test class should now be similar to this:

use MailThief\Facades\MailThief;

class ExampleTest extends TestCase

Working back in the testBasicExample function, which is where we will be making all of our testing code, the first line we will want to do is:

MailThief::hijack();

Behind the scenes, this is swapping out the default mail class (Illuminate\Mail\Mailer) with the MailThief class. This will prevent emails from actually going out and capture all the details of what email(s) we are trying to send.

So, with the emails setup to be intercepted, and to follow in Adam's footsteps, we will practice a little TDD here and make some assertions on what to expect from the email that we plan on sending.

   public function testBasicExample()
   {
      MailThief::hijack();

      $this->assertTrue(MailThief::hasMessageFor
         ('example1@enolalabs.com'));
      $this->assertTrue(MailThief::hasMessageFor
         ('example2@enolalabs.com'));
   }

The "MailThief::hasMessageFor" function will search all email addresses in the to, cc, and bcc fields. I've added one of each to make sure that each one is present, as well as added an assertion to make sure there are exactly three emails. So now, if we run "phpunit" from the project root, we should get something like this:

There was 1 failure:

1) ExampleTest::testBasicExample
Failed asserting that false is true.

/home/vagrant/Code/intro-mailthief/tests/ExampleTest.php:26

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

This is expected because we haven't made any calls to fire an email, so let's make one.

Add this to your test code:

$this->call('POST', '/', [
   'emails' => [
      'example1@enolalabs.com',
      'example2@enolalabs.com'
   ]
]);

You will want it between your hijack call and your assertions, but if you run your tests again, we should still be red (failing). Because we haven't created the route we plan on hitting, let's go make that now. Opening up your app/Http/routes.php file, create a function for posting to the "/" route, like so:

Route::post('/', function () {
});

Running the tests, we still get red, so let's add some more code until we can get to green. We will start with getting the emails from the request.

Route::post('/', function () {
   $emailRecipients = request('emails');
});

Running the tests, we still get red, mainly because we still haven't tried to send an email. So, let's add one. After adding an empty example.blade.php file under the path "resources/views/emails", we can update our function to look like this. I used collections here to go even further into Adam's later talks about collections.

Route::post('/', function () {
   collect(request('emails'), [])->each(function ($implementing) {
      Mail::send('emails.example', [], function ($m)
      use ($implementing) {
         $m->to($implementing);
      });
   });
});

Now, when we run our test, we get green (passing)! So now, we can get a little more done. Let's make sure the cc and bcc are working, so we will add some assertions to see it work.

   public function testBasicExample()
   {
      MailThief::hijack();

      $this->assertTrue(MailThief::hasMessageFor
         ('example1@enolalabs.com'));
      $this->assertTrue(MailThief::hasMessageFor
         ('example2@enolalabs.com'));
      $this->assertTrue(MailThief::hasMessageFor
         ('copy@enolalabs.com'));
      $this->assertTrue(MailThief::hasMessageFor
         ('hidden@enolalabs.com'));
}

Running the tests, we get red, so we can add some code and make our production code look like this:

Route::post('/', function () {
   collect(request('emails'), [])->each(function
         ($implementing) {
      Mail::send('emails.example',
            [implementation' => $implementing],
            function ($m) use ($implementing) {
         $m->to($implementing);
         $m->bcc('hidden@enolalabs.com');
         $m->cc('copy@enolalabs.com');
      });
   });
});

And now the test is passing, so we know the two emails we were expecting to see get an email, got one sent, and we know the "copy@enolalabs.com" and "hidden@enolalabs.com" emails were sent emails, but we don't know if they were used properly. Let's fix that:

   public function testBasicExample()
   {
      MailThief::hijack();

      $emailRecipients = [
         'example1@enolalabs.com',
         'example2@enolalabs.com'
      ];
      $this->call('POST', '/', [
         'emails' => $emailRecipients
      ]);

      $MailThief = Mail::getFacadeRoot();
      $this->assertEquals(2, $MailThief->messages->count(),
         'Expected 2 messages, got '.$MailThief->
         messages->count().'.');
      $MailThief->messages->each(function ($message, $key)
            use ($emailRecipients) {
         $this->assertEquals(1, $message->to->count());
         $this->assertEquals($emailRecipients[$key],
            $message->to->first());

         $this->assertEquals(1, $message->bcc->count());
         $this->assertEquals($message->bcc->first(),
            'hidden@enolalabs.com');

         $this->assertEquals(1, $message->cc->count());
         $this->assertEquals($message->cc->first(),
            'copy@enolalabs.com');
      });
   }

A lot has changed here! Let's go through it. First, I moved the email recipients into an array so I can access them programmatically later in my test. I then passed those same emails to the post call, like we had before. I then pulled the MailThief class from the mail façade. It's important to note that I did this after we did the "MailThief::hijack()"; anywhere after will work. This was done so we could work with the class directly vs. through the façade, which makes getting to some of the object's properties a little more difficult. Anyway, we now can work directly with all the messages and we loop over them, one by one, making sure that there are the exact number of email addresses per field (to, cc, and bcc) and they match exactly as we desire.

Now, let's add some more "real world" items, like a subject and a from field. Back to the test:

   // Inside the testBasicExample function.
   $MailThief->messages->each(function ($message, $key)
         use ($emailRecipients) {
      $this->assertEquals(1, $message->from->count());
      $this->assertEquals('noreply@enolalabs.com',
         $message->from->first());

      $this->assertEquals('This is a testing email!',
         $message->subject);

      $this->assertEquals('This is a testing email!',
         $message->subject);

      $this->assertEquals(1, $message->to->count());
      $this->assertEquals($emailRecipients[$key],
         $message->to->first());

      $this->assertEquals(1, $message->bcc->count());
      $this->assertEquals($message->bcc->first(),
         'hidden@enolalabs.com');

      $this->assertEquals(1, $message->cc->count());
      $this->assertEquals($message->cc->first(),
         'copy@enolalabs.com');
});

When we run the test, we get red with the following errors:

There was 1 failure:
1) ExampleTest::testBasicExample
Failed asserting that null matches expected
   'noreply@enolalabs.com'.

/home/vagrant/Code/intro-mailthief/tests/ExampleTest.php:30
/home/vagrant/Code/intro-mailthief/vendor/Laravel/framework/
   src/Illuminate/Support/Collection.php:205
/home/vagrant/Code/intro-mailthief/tests/ExampleTest.php:42

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

That was exactly what we expect, so let's fix it by adding the following to the email call in the routes.php file. It's important to note it that it doesn't matter where you put the following lines:

$m->subject('This is a testing email!');
$m->from('info@enolalabs.com');

After running the tests, we are back to green. So now, we can focus on the body of the email. Let's add this to the test, anywhere really, but I placed my after the email recipient tests.

   $this->assertTrue(
      $message->contains('Welcome '.$emailRecipients[$key].',
         <br /><br />'),
      'Expected to see "Welcome '.$emailRecipients[$key].',
         <br /><br />", but it was not there.'
   );
   $this->assertTrue(
      $message->contains('Thanks for signing up!<br /><br />'),
      'Expected to see "Thanks for signing up!<br /><br />",
         but it was not there.'
   );
   $this->assertTrue(
      $message->contains('Log into the application
         <a href="test.com">here</a>!'),
      'Expected to see "Log into the application
         <a href="test.com">here</a>!", but it was not there.'
   );

After running the tests we get red, so let's add the code we need to make this pass. Open up the email template from earlier (resources/views/emails/example.blade.php) and put the following code in it:

Welcome {{$implementing}},<br /><br />

Thanks for signing up!<br /><br />

Log in to the application <a href="test.com">here</a>!

After running the tests, we are back to green!

In the past, we have had to do some mocking that was ugly, hard to understand, and very fragile. Here's an example of what is required when mocking. But now, as you can see, testing emails are pretty simple and, more importantly, easy to understand with MailThief.

Good luck out there!

About the Author

Terry Rowland is a senior Web engineer at Enola Labs, an Austin, Texas-based Web and mobile development agency.

This article was contributed for exclusive use on Developer.com. All rights reserved.


Tags: PHP, TDD, Test Driven Development, Laravel, MailThief, PHPUnit




Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Enterprise Development Update

Don't miss an article. Subscribe to our newsletter below.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date
Rocket Fuel