Unit Test mail()
in PHP
mail()
is a difficult function to test because it uses sendmail
to deliver messages. This takes the whole process outside of PHP, so properly testing requires changes to your system.
Approaches to test mail()
- custom
mail()
function in a custom namespace - custom
sendmail
script - send email to
localhost
and validate content in/var/spool/mail/USER
(this may vary by system) - use mailcatcher and set the
smtp
server andstmp_port
withini_set()
. See php mail configuration or this blog post for a good example.
Use a custom mail()
function
This is my favorite approach. It requires no dependencies and is extremely simple. You could do something like this through phpunit, through my testing lib, infection php, or another test lib or custom test implementation.
- In whatever file you call
mail()
take note of the namespace. In my caseTlf\User
- Note: When calling a function, php looks in the current namespace before calling built-in functions.
- Write a file and put that same namespace at the top. Then write a
mail()
function in that file. Make sure to include this file in your test server.
Example mail()
function:
<?php
namespace Tlf\User;
/**
* For testing email
*/
function mail(string $to,string $subject,string $body,array $headers,array $params=[]){
$content = $body;
if (filter_var($to, FILTER_VALIDATE_EMAIL)===false){
$content = 'email '.$to.' is not a valid email.';
}
if (!isset($headers['From'])){
$content = "'From' header was not set";
}
if (!isset($headers['Reply-To'])){
$content = "'Reply-To' header was not set";
}
file_put_contents(
__DIR__.'/email-body-out.txt',
$content
);
if ($content!==$body)return false;
return true;
}
Benefits: I've added custom validation to make sure the email contains what i want it to contain. The php manual says 'From' is required, but i also want to gurantee reply-to. I'm also writing the email body to a file so i can verify the email body.
A simple example of validating this might be:
<?php
$expected_email = 'Who doesn\'t love a good bear?';
$email_body = file_get_contents(__DIR__.'/Server/email-body-out.txt');
if ($email_body==$expected_email)echo "Woohoo!";
else echo "uh-oh";
Though you'd probably use an assertion function in whatever testing library you prefer.
Issues
Custom sendmail
script: The custom sendmail script is an issue because you have to configure this at the system level in your php.ini
or by including a custom sendmail script in your PATH
or overwriting /usr/sbin/sendmail
. This is doable, but makes it much harder to reliably test your software on different systems. (maybe containerization makes this eaiser? Idk. I don't use docker or others)
validating /var/spool/mail/USER
: It's just a mess. I did this & kind of liked it at first, but the code was a mess & sendmail wasn't always ... instant ... so i added sleep(1)
to my test before verifying the content of the spool/mail file. Then it failed. so i changed it to sleep(2)
& it worked. Then i came back after the weekend & it was failing again without any code changes.
Custom mail()
function: Doesn't fully test mail()
. I like it. It's simple. It's self-containable, but it does not ACTUALLY test sending email. Also, it requires whatever file calls mail()
to have a namespace & for you to define a mail()
function in said namespace.
mailcatcher
: Honestly, I don't see any real issues with this. You would have to ini_set()
a couple things in your test environment that will not be set in your production environment. And you add a dependency, but that dependency is only for testing, so I don't really think it is an issue. It does mean you have to also run a localhost server in order to run your tests, but that's not a big deal either.