Getting emails through your WordPress site is important. Whether someone is informing you there’s something wrong with your products, or just dropping by to say you’re doing a great job, you need to make sure you receive those emails.
Too often, our tests for if emails work on a WordPress site or in a WordPress plugin involve filling out a form, checking our email and seeing if it looks right. That’s not efficient and requires a proactive investment of human time. Let’s write a program to do it better and more consistently instead.
This article is about using testing emails. Most of it will be about writing automated integration tests with phpunit. There is also a short section about related tools. That covers local testing and cross-client testing lightly. Those are layers I’d recommend looking into adding after this layer is in place.
The tests in this article will be using the WP_UnitTestCase, which extends PHP Unit’s base test case. Before getting started, you’ll need to have tests set up the way that WP CLI would set them up for you. This was covered in a recent Torque article Iain Poulson and you also might want to read Pippin Williamson’s series on unit testing in WordPress.
The Mock PHPMailer
When writing tests for code that integrates with other systems it’s common to create “mock” objects. These are objects that have the same public API as the object they are standing in for, but don’t create side effects or add additional dependency complications to the test. A true mock is really only possible when all the code involved is object-oriented and every object’s public API is defined by an interface. That doesn’t describe WordPress.
That doesn’t mean you can’t create mocks in WordPress. The libraries BrainMonkey and wp_mock help with mocking many WordPress APIs, including hooks.
WordPress’ unit tests ship with a mocking tool for the PHPMailer instance that wp_mail uses. When using WP_UnitTestCase, an instance of MockPHPMailer is substituted for the global instance of PHPMailer that is normally used by wp_mail(). MockPHPMailer extends PHPMailer, and also tracks all of the email sent through it. That allows us to test the result of wp_mail and PHPMailer given arguments passed to wp_mail. This article is mainly about how to setup and use that.
The accuracy of a mock limits its usefulness. By using the mock for wp_mail that core uses, we know that our tests are as accurate as WordPress core’s wp_mail tests. Doesn’t make them perfect. Practically speaking, it does make it a pretty reliable way to test. At the end of this article, there are links to other tools you can use to add additional testing layers on top of this method, but I would start with this method.
We can retrieve that instance using the function tests_retrieve_phpmailer_instance(). We’ll look at how to do that now.
Creating A Test Case
It’s a common pattern to have one abstract class in your plugin’s test that is your base test case. That class would extend WP_UnitTestCase. Then, if needed, you can extend that class to create a base for a specific type of test.
Let’s start by creating Email_Test_Case:
<?php /** * Test case -- step 1 */ abstract class Email_Test_Case extends WP_UnitTestCase{ /** @inheritdoc */ public function setUp(){ parent::setUp(); } /** @inheritdoc */ public function tearDown(){ parent::tearDown(); } }
Right now, this doesn’t add anything new, it’s just a start. The setUp and tearDown methods that we are overriding from the parent class are used to reset everything in between each individual test. Since WordPress uses a global for PHPMailer, the mock mailer can track every email sent. That’s useful, but also we want to make extra certain that everything is reset in between tests.
We can use the function reset_phpmailer_instance to put a new instance of MockPHPMailer in the global used by wp_mail. Let’s do that in setup and teardown, just to be safe.
<?php /** * Test case -- step 2 */ abstract class Email_Test_Case extends WP_UnitTestCase{ /** @inheritdoc */ public function setUp(){ parent::setUp(); $this->reset_mailer(); } /** @inheritdoc */ public function tearDown(){ parent::tearDown(); $this->reset_mailer(); } /** * Reset mailer * * @return bool */ protected function reset_mailer(){ return reset_phpmailer_instance(); } /** * Get mock mailer * * Wraps tests_retrieve_phpmailer_instance() * * @return MockPHPMailer */ protected function get_mock_mailer(){ return tests_retrieve_phpmailer_instance(); } }
I created a method for resetting the mock for two reasons. First, I’m repeating code. More importantly, I may later want to extend this class to run tests using a different way of mocking PHPMailer or a different way of sending email than PHPMailer. This allows subclasses to change this behavior in those situations.
Those reasons explain why I also created a wrapper around the function for retrieving the MockPHPMailer instance. Also, my way has a more explicit return phpDocBlock annotation than core uses, which makes using it in phpStorm easier.
Writing Integration Tests For WP Mail
Before we go further, we’ll need something to test. Let’s create a function that sends an email, and lets us change who is goes to, the subject and the body:
function slug_send_email( $to, $subject, $message, $headers = '' ){ wp_mail( $to, $subject, $message, $headers ); }
This function doesn’t return anything. We can’t unit test it. But we can test the effects it has on the system it integrates with — WordPress’ phpmailer. Let’s create an integration test for each of the three features — recipient, subject and message.
All of these tests can live in one class, let’s create it, extending our test case:
Now we can write some tests.
Testing The Email Subject
Let’s start with the email subject because it’s the simplest. We’ll need to send the message then compare the subject that was captured mock mailer object’s subject to what we expected to be sent.
For this, we can use the mock mailer’s get_sent() method. This returns the first message sent through this mock mailer. We can optionally ask it for other messages, but we’re intentionally resetting the mailer between tests.
The get_sent() method returns an array of information about the email. One of the indexes is the subject. We can use that to get the subject of the “sent” message.
<?php public function test_subject(){ $to = 'Someone W. Someone <[email protected]>'; $subject = 'subject'; $message = 'message'; slug_send_email($to, $subject, $message ); $mailer = tests_retrieve_phpmailer_instance(); $this->assertSame($subject, $mailer->get_sent()->subject ); }
In this test, we set up all of the arguments for the function we’re testing in variables. Then we use those to call the function. Then we capture the mock mailer instance and get its subject. Keeping the arguments for our function in variables makes it easier to use them as the expected value for an assertion.
Testing The Email Body
Testing the email body is going to be similar. I don’t think testing with just one line of content is a fair test. So for this test, we’ll use multiple lines. That does make things a little complicated, as comparing whitespace is tricky.
One of the extra assertions we get from using WordPress’ test case, is the assertDiscardWhitespace test. This tests the contents of strings as being the same, minus whitespace:
Testing The Email Recipient
Testing the email recipient is tricky. Emails can be provided as an email address or in Name <[email protected]> form. Also, we can pass a single email address or an array of one or more email addresses to the to the argument of wp_mail and therefore the to argument of the function we’re testing.
We’re going to need one test for each of these cases. Let’s start simple, no name, just email. For this test, we use the mock mailer’s utility function get_recipient() to get the to. In this context, that function returns a standard class object. We can check the address property:
Once we know that works, we can add a layer of complication by setting the name and email. We’ll need a similar assertion as the last test, but this time we’ll check name instead of the address.
Now for our last test, we’ll send the email to multiple recipients.
Going Further
At this point, you should be wondering what if the multiple recipients are in a mix of the two forms. Or what if some of the messages in the array are invalid. Those are good thoughts. You should add those tests.
What about cc, bcc, attachments and other headers? I’d recommend taking a look at core’s mail tests and see how you can use a similar approach with the actual code you’re testing.
Other Email Testing Tools
This article is about integration testing WordPress emails. We’re testing your code’s integration with WordPress’ email feature. That’s an important part of testing email functionality. One advantage of this approach — that no external servers are needed — is also a limitation. Once these types of tests pass, it is important to also test real servers and real clients.
Mailhog is a useful tool for local development. It prevents emails from actually sending, instead, they are stored locally. You get a simple, and extensible UI that provides an email client-like way to see emails sent in your local environment.
MailTrap is a service that provides an actual SMTP server designed for use in automated testing. To be honest, this is the one tool in this article I haven’t used. I’m putting it in because it’s on my list of tools I’d like to explore more as I improve this type of testing in my stack, so I wanted to share.
What I have used, and have gotten a lot of value out of, as part of manual QA is used Email On Acid. This service is like Browserstack, but for email. You send your email to one address and they show you what it looks like in all of the email clients and even highlight possible issues in a useful summary.
Email Is Hard, Testing Help
It’s not naming things or cache invalidation, but I have found emails to be one of the trickiest things to work with as a developer. I spend a lot of time with them given what I do — contact form plugin and email web app.
These are the tools that helped me take control of the situation when testing by sending myself and email didn’t scale anymore. It’s gotten more complex over time. Automated integration testing and manual testing of designs — when they change — using Email On Acid — or a similar tool is probably sufficient. Whatever you decide you need to keep your WordPress email system reliable, I hope this helps.
No Comments