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 and stmp_port with ini_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.

  1. In whatever file you call mail() take note of the namespace. In my case Tlf\User
    • Note: When calling a function, php looks in the current namespace before calling built-in functions.
  2. 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.