WordPress Plugins Unit Tests … without WordPress – Part 2

This is the second part of my deep insight into Unit Tests with WordPress Code without loading the whole WordPress environment.

In the first part of this 2-part series, we saw how unit tests are, at their core, a very simple concept that does not require complex frameworks or bloated libraries, it is all about executing some code and get a feedback. We even saw how a tweet-sized function can be used to write all the unit tests we might need.

After that, we analyzed both the convenience and the problems of running unit tests for WordPress plugins without loading WordPress, and then saw how stubs can allow us to do it, but they have some issues: they need to be written, they offer surface to bugs, they need to be maintained.

In this second part we will see a different approach and tools to that can help us overcome this issues.

If you missed the first article on December 9th, please go read the first part first, because we will refer to concepts and code sample introduced there.

Mock Around The Clock

In the first part we said that what’s matter when testing plugins is not to test that the WordPress code work (it is assumed to work), but to test that custom code uses WordPress functions in the proper way.

For that purpose we seen a couple of examples where thanks to stubs we were able to run tests of WordPress plugin code without loading WordPress.

But… did we actually test that WordPress functions were used correctly?

For example, did we test what happesn if they were called more than once or not called at all? Did we were able to test the arguments that were passed to those WordPress functions?

The stub we wrote were very simple (on purpose) and not capable to catch this sort of improper usages of WordPress code, and that means that when the our code would be ran in production with WordPress being loaded, there’s the possibility of some breakage.

What we would need is a stub that makes the test fail if the function which is expected to be called is not actually called, or it’s called a wrong number of times, or it’s called with wrong arguments. This would (to some degree) guarantee that, if the tests pass, when replacing the stub with the real thing everything works.

I just described a mock object. In facts, that’s an object that implements the same interface(s) of the original object, and allows to test that the method calls it receives are compliant to a set of expectations written to ensure compliance with the original object.

Often times, mock objects are not stored in the filesystem, they are “virtual” objects created on runtime with the help of some library, based on given expectations.

Yesterday, in first part of the article we wrote an example of a Clock object, and we saw how to use a stub to test it. Now let’s see how a mock object could be used instead:

<?php
// Mockery is installed via Composer and requires autoload.
require_once __DIR__.'/vendor/autoload.php';
require_once __DIR__.'/test-framework-in-a-tweet.php';
require_once '/path/to/clock-and-gong.php';


it( "should gong 3 times at 3 o'clock.", function () {
  
    $threeOClock = \Mockery::mock('Clock');
    $threeOClock
        ->shouldReceive('hours')
        ->once()->withNoArguments()->andReturn(3);
    $threeOClock
        ->shouldReceive('minutes')
        ->once()->withNoArguments()->andReturn(0);
    $threeOClock
        ->shouldReceive('seconds')
        ->once()->withNoArguments()->andReturn(0);
  
ob_start();
maybeGong( $threeOClock );
$result = ob_get_clean() === "GONG GONG GONG";
  
    \Mockery::close();
  
    return $result;
} );

A PHP library called Mockery is used above to create a mock of the same Clock interface we saw yesterday.

The mock object does the same job of the ClockStub we saw, but:

  • no need of an additional class be present in the codebase: the mock object is created just-in-time inside the tests itself.
  • The code necessary is far less (22 lines VS 4), and offers pretty much zero surface to bugs. In the 4 lines of code it requires, all the code belongs to the Mockery library, which is tested separately.
  • The mock is more effective than the stub, because it not only responds to method calls in a pre-defined way, but is also capable to ensure that the methods are called a specific number of times with specific number and type of arguments.

I used Mockery because I like its DSL that reads as plain English and the fact that it can be used with any test framework (even with our tiny TestFrameworkInATweet introduced yesterday), but it is not the only PHP library that allows to create mock objects.

PHPUnit, the most popular PHP test framework, has a mock library integrated. Phake and AspectMock are other alternatives.

A technical note: Mockery, which will be used for the rest of the article, requires that Mockery::close() is called at the end of each test. For this reason the rest of the article I will refer to the following file:

<?php
namespace m;

require __DIR__.'/vendor/autoload.php';
require __DIR__.'/test-framework-in-a-tweet.php';
  
function it($m,$p) { $d=debug_backtrace(0)[0];
    try {
        ob_start();
        \it($m,$p);
        $output = ob_get_clean();
        \Mockery::close();
        echo $output;
    } catch(\Throwable $t) {
        $GLOBALS['e'] = true;
        $msg = $t->getMessage();
        echo "\e[31m✘ It $m\e[0m ";
        echo "FAIL in: {$d['file']} #{$d['line']}. $msg\n";
    }
}

that extends the TestFrameworkInATweet introduced yesterday with Mockery features. Thanks to it we can now write tests like this:

<?php
// this file contains the 19 lines in the snippet above
require __DIR__.'/test-framework-in-a-tweet-mockery.php';

m\it( "should work with Mockery", function () {
    $mock = Mockery::mock();
    // this is not satisfied, so this test should fail
    $mock->shouldReceive('test')->once();
    return true;
});

and the output of test above should be:

✘ It should work with Mockery, which means this test should fail. FAIL in: /path/to/test-file.php #9. Method test(<Any Arguments>) from Mockery_0 should be called exactly 1 times but called 0 times.

A Lot To Mock in WordPress

Mockery (and other PHP mocking libraries) allow to mock objects, and this is a good help if we want to test WordPress plugins without loading WordPress.

Last time I checked, around 51% of the more than 330000 lines of WordPress PHP code (and libraries it embeds) are written into 368 classes (and 6 interfaces).

Let’s see an example on how with Mockery and our just improved test framework we can test WordPress code that uses objects.

First, some WordPress code:

<?php
namespace MyCompany\MyPlugin;

class ProductsQuery {

    private $query;
    private $paged = 0;

    public function __construct( \WP_Query $query = NULL ) {
        $this->query = $query ? : new \WP_Query();
    }

    public function next_products(): array {
        $this->paged ++;
        $this->query->query( [
            'post_type'      => 'product',
            'posts_per_page' => 10,
            'orderby'        => 'title',
            'order'          => 'ASC',
        ] );
        if ( ! $this->query->have_posts() ) {
            return [];
        }

        return $this->query->posts;
    }
}

This is a class that wraps WP_Query and pass specific arguments to query a “product” post type.

The class is all about calling WP_Query methods. And we don’t want to test WP_Query, but we want to test that WP_Query methods are called properly and that the output of calling our class methods matches the expectations.

Let’s do it:

<?php
require __DIR__.'/test-framework-in-a-tweet-mockery.php';
require '/path/to/MyCompany/MyPlugin.php/ProductsQuery.php';


m\it( "should return no products if no products.", function () {
  
    $query_mock = Mockery::mock( 'WP_Query' );
    $query_mock
        ->shouldReceive( 'query' )
        ->once()
        ->with( \Mockery::type( 'array' ) );
    $query_mock
        ->shouldReceive( 'have_posts' )
        ->once()
        ->withNoArgs()
        ->andReturn( false );

    $query = new MyCompany\MyPlugin\ProductsQuery( $query_mock );

    return $query->next_products() === [];
} );


m\it( "should get first 10 products.", function () {

    $ten_posts = array_map(
        function() { return Mockery::mock('WP_Post'); },
        range(0, 9)
    );

    $query_mock = Mockery::mock( 'WP_Query' );
    $query_mock
        ->shouldReceive( 'query' )
        ->once()
        ->withArgs( function ( array $args ) {
            return
                ($args[ 'post_type' ] ?? '') === 'product'
                && ($args[ 'posts_per_page' ] ?? '') === 10;
            } )
            ->andSet( 'posts', $ten_posts );
    $query_mock
        ->shouldReceive( 'have_posts' )
        ->once()
        ->withNoArgs()
        ->andReturn( true );

    $query = new MyCompany\MyPlugin\ProductsQuery( $query_mock );

    return $query->next_products() === $ten_posts;
} );

Thanks to Mockery, without loading WordPress code above tests that the our class returns values according to what WP_Query returns, and that WP_Query methods are called exactly how they are expected to be called.

I suggests you to have a look at Mockery documentation because it has a lot of features I’m not going to cover in this article.

By the way, it’s nice, isn’t it?

Yes, but… if it’s true that WordPress has few hundreds of classes, it has more then 2900 functions and without an effective way to mock/stub them, no way we can test WordPress plugins code without loading WordPress.

The Clever Monkey

Mock objects, works for… objects, but we WordPress developers need something similar for functions.

We saw in the first part of the article that when WordPress functions are not defined, it is quite easy to write stubs for them. However, when we write tests in real world, we want to write different stubs based on what we want to test, but PHP does not allow to re-define functions.

It appears clear that what we need is:

  1. a way to apply the same concepts of objects mocks to functions.
  2. a way to re-define functions.

The first point is pretty simple to obtain. A function is not that different from an object with a single method. So we just need some code that takes the name of the function we want replace, creates an object for it with a single method, then adds Mockery expectations to that object.

The second point is quite harder. Since 2010 it is available for PHP an open source library called Patchwork that allows to re-define PHP functions on runtime (runtime re-definition of functions, is sometimes referred as “monkey patching”).

Even if PHP does not allow to monkey-patch functions source code, Patchwork uses a trick (I save you the tech details) that makes possible to re-define PHP functions.

So now we have all the ingredients that might help in test WordPress functions without loading WordPress, we just need to “cook” them together.

Guess what, someone already did.

Almost three years ago I started developing Brain Monkey which started as a single class of around 100 lines of code. Brain Monkey at its core do pretty much what I described above: it provides a “bridge” between the function redefinition of Patchwork and the mock creation of Mockery.

Brain Monkey documentation tells us that we need to do some setup to use it: we need to call Brain\Monkey\setUp() before each test, and Brain\Monkey\tearDown() after each test.

Brain Monkey uses Mockery, so its tearDown() function already calls Mockery::close().

Because I want to show you how to use Brain Monkey I’m going to write few lines of code to extend our micro test framework with Brain Monkey capabilities:

<?php
namespace bm;

require_once __DIR__.'/vendor/autoload.php';
require_once __DIR__.'/test-framework-in-a-tweet.php';
  
function it($m,$p) { $d=debug_backtrace(0)[0];
    try {
        \Brain\Monkey\setUp();
        ob_start();
        \it($m,$p);
        $output = ob_get_clean();
        \Brain\Monkey\tearDown();
        echo $output;
    } catch(\Throwable $t) {
        $GLOBALS['e'] = true;
        $msg = $t->getMessage();
        echo "\e[31m✘ It $m\e[0m";
        echo "FAIL in: {$d['file']} #{$d['line']}. $msg\n";
    }
}

Let’s see an example of real world WordPress code that we will test soon. It is class that manages the registration and the rendering of a shortcode:

<?php
namespace MyCompany\MyPlugin;

class LastPostShortcode {

    const DEFAULTS = [
        'title'     => TRUE,
        'date'      => TRUE,
        'content'   => FALSE,
        'post_type' => 'post',
    ];

    public function register() {
        add_shortcode( 'last_post', [ $this, 'render' ] );
    }

    public function render( $args ) {
      
        $defaults = self::DEFAULTS;
        $atts = shortcode_atts( $defaults, $args, 'last_post' );

        $query = new \WP_Query([
            'post_type'      => $atts[ 'post_type' ],
            'posts_per_page' => 1,
            'orderby'        => 'date',
            'order'          => 'DESC',
        ]);
      
        if ( ! $query->have_posts() ) {
            return;
        }

        $query->the_post();

        $out = sprintf( '<div class="last_post" id="%s">', get_the_ID() );
        if ( $atts[ 'date' ] ) {
            $out .= sprintf('<p class="date">%s</p>', get_the_date() );
        }
        if ( $atts[ 'date' ] ) {
            $out .= sprintf('<h1>%s</h1>', esc_html( get_the_title() ) );
        }
        if ( $atts[ 'content' ] ) {
            $out .= get_the_content();
        }
      
        wp_reset_postdata();

        return $out . '</div>';
} } 

Like any average WordPress code, there are a lot of function calls.

For some of them, e.g. wp_reset_postdata, we are probably interested only in the fact that the function is defined. For such functions, if we would write the stub “by hand” it would be something like:

function wp_reset_postdata() {}

just enough to not cause a fatal error because of undefined function.

For other functions, like esc_html, for testing purposes we would just to return the first argument they receive unchanged. The fact that those functions actually escape values is not something that we should test in our plugin (we assume they work). For those functions the “by hand” stub would look like:

function esc_html( $arg ) { return $arg; }

Then we have functions for which we don’t really need to control how they are called (or how many times), but we need control on the return value. For example:


function get_the_ID() { return 123; }
function get_the_title() { return 'Test'; }

And finally, there are functions for which we need full control: if, when, how and how many times they are called.

For example, for add_shortcode we probably want to check that is actually called once, that the arguments are the right ones and called in proper order. The same for shortcode_atts, because we want to check that we pass arguments in the right order and that the 3rd argument matches the 1st argument add_shortcode.

We will see soon how Brain Monkey will help us to obtain what we want without writing by hand any stub.

Regarding objects, the class we are going to test uses a single instance of WP_Query, but this time instead of having an instance of it accepted in constructor, WP_Query is instantiated directly inside the render method.

Normally, I would say this is a bad practice. The problem here is that WordPress classes, more often than not, does not really follow good OOP principles. For example, WP_Query accepts query arguments in the constructor and a database query is performed as soon as the object is created. For that reason it is quite common to have WP_Query instances created inside other classes, just before they are used.

How can we possibly test this thing? Mockery has a feature for us. Thanks to its overload feature, Mockery allow us to intercept calls to new keyword and replace the class being instantiated with a mock object.

Let’s try it out:

<?php
// Load Patchwork as early as possible
require_once __DIR__.'/vendor/antecedent/patchwork/Patchwork.php';
require_once __DIR__.'/test-framework-in-a-tweet-brain-monkey.php';
require_once '/path/to/MyCompany/MyPlugin/LastPostShortcode.php'

use Brain\Monkey;
use MyCompany\MyPlugin\LastPostShortcode;


bm\it( 'should render shortcode.', function () {

    $args = [ 'title' => FALSE, 'content' => TRUE, ];
    $all_args = array_merge( LastPostShortcode::DEFAULTS, $args );

    // Full control on `shortcode_atts`
    Monkey\Functions\expect( 'shortcode_atts' )
        ->once()
        ->with( LastPostShortcode::DEFAULTS, $args, 'last_post' )
        ->andReturn( $all_args );

    // We want these be defined and know their return value
    Monkey\Functions\stubs([
        'get_the_ID'        => 123,
        'get_the_date'      => '15/12/2017',
        'get_the_title'     => 'Test Title',
        'esc_html'          => null,
        'get_the_content'   => '<p>The content!</p>',
        'wp_reset_postdata' => TRUE
    ]);

    // Overload WP_Query
    $query_mock = Mockery::mock( 'overload:WP_Query' );
    $query_mock->shouldReceive( 'have_posts' )
        ->once()->andReturn( TRUE );
    $query_mock->shouldReceive( 'the_post' )
        ->once();

    // Perform the method!
    $sh = new LastPostShortcode();
    ob_start();
    $sh->render( $args );
    $rendered = ob_get_clean();

    // Let's test the rendered string is what we expect:
    $has_id       = strpos( $rendered, 'id="123"' );
    $has_date     = strpos( $rendered, '15/12/2017' );
    $has_no_title = strpos( $rendered, '<h1>' ) === FALSE;
    $has_content  = strpos( $rendered, '<p>The content!</p>' );

    return $has_id && $has_date && $has_no_title && $has_content;
} );

We tested successfully a real word WordPress plugin class without loading WordPress.

Surely, the test is quite big!

The limited features of out micro test framework don’t help here.

If we were using a more advanced framework we could have a much smaller test: we could extract all the functions stubbing to a separate test class method. Same for query overloading. Finally, instead of performing all the assertions at once, we could have used different tests.

Moreover, we need to manage output buffer by hand, whereas advanced frameworks have this feature embedded.

To give you an idea, in PHPUnit a test for the same class could be something like this:


public function test_post_content_is_renderered_if_enabled() {

    $args = [ 'content' => true ];
    
    $this->expect_shortcode_atts_for( $args )
    $this->stub_functions();
    $this->overload_query();
    
    $this->expectOutputRegex('~<p>The content!</p>~');
    
    $sh = new LastPostShortcode();
    $sh->render( $args );
}

Do you like Brain Monkey so far? There’s more about it!

Hooked on Testing

With Mockery providing all the methods to mock and stub objects, and Brain Monkey bringing those features to functions we have everything we need to test WordPress plugin without loading WordPress.

however, there are a set of functions that we find everywhere in WordPress plugins, and mock them every time is quite tedious and awkward. I’m talking about the plugin API functions.

Ensure an action or a filter has been added and with which callable, testing that an action as been fired and with which arguments, or that a filter as been applied and maybe respond to it… these are all operations that testing real world plugins we will feel the the need to do many and many times.

Sure we can use Brain Monkey Functions\expect to create function stubs for add_action, add_filter, do_action, apply_filters, and all the other functions of plugins API, but that would require a lot of bootstrap code for each test, reducing (or nullifying) the convenience of running test without loading WordPress.

Luckily, Brain Monkey has an API designed just to test WordPress plugins API.

The first good news is that Brain Monkey defines already all the functions of plugins API in a way that is 100% compatible WordPress real code.

That means that if the plugin code we want to test uses any of the functions of the plugin API, we could test it without mocking or stubbing anything.

For example:

<?php
namespace MyCompany\MyPlugin;
  
function create_initializer() {
  
    if ( ! did_action( 'init' ) ) {
        return null;
    }
  
    do_action( 'my_plugin_init' );
  
    $init_class = apply_filters(
        'my_plugin_init_class',
        Initializer::class
    );
    $initializer = new $init_class();
  
    add_action( 'template_redirect', [ $initializer, 'init' ] );
  
    return $initializer;
}

This looks like some real world WordPress code.

Thanks to Brain Monkey we could do something like this:

<?php
require_once __DIR__.'/vendor/antecedent/patchwork/Patchwork.php';
require_once __DIR__.'/test-framework-in-a-tweet-brain-monkey.php';
require_once '/path/to/MyCompany/MyPlugin/functions.php';
require_once '/path/to/MyCompany/MyPlugin/Initializer.php';
 
use MyCompany\MyPlugin;


bm\it(
    'should not initialize anything if WP is not initialized.',
    function () {
        return MyPlugin\create_initializer() === null;
    }
);


bm\it(
    'should instantiate Initializer if WordPress is initialized.',
    function () {
        do_action( 'init' );
        $initializer = MyPlugin\create_initializer();
        return $initializer instanceof MyPlugin\Initializer;
    }
);

We ran our tests just like WordPress was loaded, without any mock or stub, and everything worked as expected!

The function under test uses 4 functions of plugins API (did_action, do_action, apply_filters, and add_action) and we even used do_action it tests code… and everything just worked.

Even if this is amazing already (sorry, I’m biased) the fact we can easily set expectations on those hooks or even respond to filters, is even better.

Some other tests we can write:

use Brain\Monkey\Action;
use Brain\Monkey\Filters;


bm\it(
    'should not fire my_plugin_init if WP is not initialized.',
    function () {
        Actions\expectDone( 'my_plugin_init' )
            ->never();

        MyPlugin\create_initializer();

        return true;
    }
);


bm\it(
    'should fire my_plugin_init once if WP is initialized.',
    function () {

        Actions\expectDone( 'my_plugin_init' )
            ->once()
            ->withNoArgs();

        do_action( 'init' );

        MyPlugin\create_initializer();

        return true;
    }
);


bm\it(
    'should add hook initializer method to template redirect',
    function () {
        Actions\expectAdded( 'template_redirect' )
            ->once()
            ->withArgs( function($cb) {
                return
                    is_a( $cb[0], MyPlugin\Initializer::class )
                    && $cb[1] === 'init';
            } );

        do_action( 'init' );

        MyPlugin\create_initializer();

        return true;
    }
);


bm\it(
    'should use a different initializer class if filtered.',
    function () {

        $mock       = \Mockery::mock( MyPlugin\Initializer::class );
        $mock_class = get_class( $mock );

        Filters\expectApplied( 'my_plugin_init_class' )
            ->once()
            ->with( MyPlugin\Initializer::class )
            ->andReturn( $mock_class );

        do_action( 'init' );

        return is_a( MyPlugin\create_initializer(), $mock_class );
    }
);

In short: we will be able to test all the plugin API functions with the finest level of control, and with a syntax that is very readable and the consistent with the one we use to create mock objects with Mockery.

If you have a look at Brain Monkey documentation for testing added hooks and fired hooks you’ll find more features to power your tests.

There’s more…

Before ending the article, I want to left here that plugin API functions are not the only functions that are already mocked by Brain Monkey for you.

If your code use any of the following functions:

  • __return_true
  • __return_false
  • __return_null
  • __return_empty_array
  • __return_empty_string
  • __return_zero
  • trailingslashit
  • untrailingslashit

You don’t need to mock them, or anything… they will just work just like they do in WordPress context.

In short, thanks to Mockery and Brain Monkey we have the possibility to write unit tests for our plugin without loading WordPress in a way that is very easy to start with and produces quite maintainable tests.

Do you have questions? Feedback? Do you think I missed something? Do you think I am completely wrong in something or anything? The comment section is here to serve you, and we are listening…

PS: Brain Monkey is open source and very open to contributions.