PHPUnit Tests for WordPress Plugins: wp_mail()

by Daniel Convissor at 2012-11-06 14:22:00

PHPUnit is a great way to improve and maintain application quality. This post summarizes how I use PHPUnit to test the behavior of WordPress' wp_mail() function in the Object Oriented Plugin Template Solution.

Before getting into the nitty gritty, let me explain two terms I'll be using. First is "parent class." It holds helper properties and methods for use by multiple test classes.

abstract class TestCase extends PHPUnit_Framework_TestCase {}

The second term is "test class." It extends the "parent class" and contain the test methods that PHPUnit will execute.

class LoginTest extends TestCase {}

Okay, now with the show...

The wp_mail() function is "pluggable." Site owners can override such functions (because WordPress declares their version of a given function only if it hasn't been implemented yet). So my test framework takes advantage of this by declaring one first. All my version does is call a static method in the "parent class."

function wp_mail($to, $subject, $message) {
    TestCase::mail_to_file($to, $subject, $message);
}

My "mail" method in the "parent class" writes the message contents to a temporary file for later comparison. But before doing that, it makes sure that the mail function is actually being called at an expected time by checking that the $mail_file_basename propery has been set.

public static function mail_to_file($to, $subject, $message) {
    if (!self::$mail_file_basename) {
        throw new Exception('wp_mail() called at unexpected time'
                . ' (mail_file_basename was not set).');
    }

    if (!self::$temp_dir) {
        self::$temp_dir = sys_get_temp_dir();
    }

    // Keep Windows happy by replacing :: in method names with --.
    $basename = str_replace('::', '--', self::$mail_file_basename);
    self::$mail_file = self::$temp_dir . '/' . $basename;

    $contents = 'To: ' . implode(', ', (array) $to) . "\n"
            . "Subject: $subject\n\n$message";

    return file_put_contents(self::$mail_file, $contents, FILE_APPEND);
}

Now we need to create the file containing the expected message. The files are generally named for the test method where the mail is being triggered from, using the format produced by the __METHOD__ magic constant but with :: replaced with -- to keep Windows from freaking out. For this example it's LoginTest--test_notify_login.

Since the text of the actual messages can be composed with translated strings, we need to provide separate expected message files for each supported translation. The files for a given translation go into a subdirectory of tests/expected named for the WPLANG to be tested. The default directory is tests/expected/en_US.

Here's the expected file for this example, using format placeholders for strings that can vary in different environments:

To: %a
Subject: howdy

%s just logged in to %s.

The actual test in the "test class" is pretty straight forward. The comments explain what's going on.

public function test_notify_login() {
    // Set the name of the file holding the expected output.
    self::$mail_file_basename = __METHOD__;

    // Call the method that generates the mail.  (See declaration, below.)
    self::$o->notify_login($this->user_name);

    // Compare the generated message to the expected one.  (See below.)
    $this->check_mail_file();
}

The plugin's method that generates the email:

protected function notify_login($user_name) {
    $to = get_site_option('admin_email');
    $subject = 'howdy';
    $blog = get_option('blogname');

    // __() passes the string though the translation process.
    $message = sprintf(__("%s just logged in to %s.", self::ID),
            $user_name, $blog) . "\n";

    return wp_mail($to, $subject, $message);
}

Back in the "parent class," there is a method for comparing the files containing the expected and actual messages. The first thing it does is check that wp_mail() was really executed.

protected function check_mail_file() {
    // Ensure the actual output was generated.
    if (!self::$mail_file) {
        $this->fail('wp_mail() has not been called.');
    }

    $basedir = dirname(__FILE__) . '/expected/';
    $locale = get_locale();
    if (!file_exists("$basedir/$locale")) {
        $locale = 'en_US';
    }

    // Placate Microsoft.
    $basename = str_replace('::', '--', self::$mail_file_basename);

    // Use PHPUnit's file diff method.
    $this->assertStringMatchesFormatFile(
        "$basedir/$locale/$basename",
        file_get_contents(self::$mail_file)
    );
}

If everything worked as planned, assertStringMatchesFormatFile() will tell PHPUnit that the test passed. If not, it'll produce a diff between the expected and actual output. Do note, there's a bug in PHPUnit 3.6 that shows all lines as having a diff, not just the ones that actually have a diff. This is fixed in PHPUnit 3.7.

Well, that's it. Let me know if this was helpful. To examine the full framework, download the Object Oriented Plugin Template Solution from WordPress' plugin directory.

Tags: wordpress, phpunit, php

View all posts

Email me a comment:

(I'll append it here when I get a chance.)

Your Name:
Your Email Address:
Subject:
Message:
(1000 chars. max.)