Josh Pollock
Posted on November 30, 2020
In general, I make the distinction between unit and integration testing based on dependencies. A unit test should test one "unit" of code only, just one class or function, with no external dependencies -- WordPress or MySQL.
Unit testing with mocks can be very useful. It can also create false confidence beacuse it assures your code works when the dependencies work as you assume they will. So don't forget to use integration tests.
One way to make your code more testable is to inject dependent functionality into a class. Then you can mock those classes with a different class implementing the same interface, or use a mocking library such as Mockery.
That's not always possible, or not practical, if the functionality you need to mock out is a function from WordPress core. In those cases, monkey patching is a better solution. "Monkey patching" is the process of swapping the definition of a plugin at runtime, this case during testing.
Instead of testing with the actual function -- that would be an integration test -- we replace it. When we use a monkey patching library or a mocking library we can ensure that the code being tested calls the dependency, calls it with the right value and does the right things with the return values.
There is a WordPress-specific tool called Brain Monkey, that can do this for us. In this post, I will show you how to do that. Both Mockery and Brain Monkey will be setup if you choose to add unit tests, when creating a WordPress plugin with Plugin Machine.
If you need a good way to try these tests out, that's good place to start. Alternatively, you can copy my example tests into your plugin.
You just need to install Brain Monkey first:
composer require brain/monkey:2.* --dev
Unit Testing That WordPress Hook Callbacks
I often get asked how to test a class that calls add_action()
and add_filter()
in the constructor and then has callback functions in the same class. The best advice I can give you is don't do that.
Instead, write classes, or even better functions for your callbacks that are totally de-coupled from the WordPress plugins API.
For example, imagine a plugin that adds a call to action to every post. If I create a class that can be used as a callback for the the_content
filter.
<?php
class ContentFilters{
protected $cta;
public function __construct($cta)
{
$this->cta = $cta;
}
public function callback($content){
return $content . sprintf( '<p>%s</p>', $this->cta );
}
}
This is a super basic example, but if the callback function for an action or filter does something, you should be able to test it by passing the arguments to the function and testing the side effects or return values. If there are a lot of side effects, that's probably better suited to an integration test.
Because this is a class with one responsibility, I can test it's business logic without worrying about the integration with the WordPress plugins API.
class ContentFiltersTest{
public function testAddCta(){
$adder = new ContentFilters('Hi Roy');
$this->assertSame('Before<p>Hi Roy</p>',$adder->callback('Before');
}
}
Unit Testing WordPress Functions Called By Hooks
In general structuring your code for easy testing, for example decoupling hooks from hook callbacks is a good practice. So that's an advantage of testing, it makes path of least resistance -- code that's easy to test -- the good path.
But what about a function that sets up enqueuing scripts? That's a good example of something I test, just to prevent big mistakes that break everything. Here is an example of that kind of function:
add_action('wp_enqueue_scripts', 'callback_for_registering_scripts');
function callback_for_registering_scripts()
{
wp_register_script(
//...
);
wp_localize_script(
//...
);
}
I can use Brain Monkey's expect()
function to ensure the functions in my callback get called:
public function testRegisterAndEnqueue()
{
callback_for_registering_scripts();
\Brain\Monkey\Functions\expect('wp_register_script')->once();
\Brain\Monkey\Functions\expect('wp_localize_script')->once();
}
You could also use expect to make sure that add_action()
is called. But that would be a different test, which you could write with expect. There is also a specialized library for mocking WordPress hooks.
Unit Testing Interactions With WordPress Core Functions
So far, we've just tested that functions are called. That's nice, but often times we have logic following a core function call. That's what we need to test: our business logic. For example, let's say you have a class that creates a post:
class Storage {
public function save(string $title){
$id = wp_insert_post(
['post_title' => $title]
);
if( ! is_numeric($id)){
throw new \Exception('Could not create');
}
return $id;
}
}
If you were to unit test this, without monkey-patching wp_insert_post()
, you'd get an error. Our goal isn't to test wp_insert_post
. Our goal is to test the unique business logic of the plugin. In this case, that is the conditional and whether or not it throws an exception.
We can do that with the when()
function. When we call when()
, we can chose what to return. I will test once where it returns an integer, which is what happens when a post is saved. I will test again and this time have it return an object. That test expects an exception to be thrown.
These two tests cover what happens if things go right and what happens if they go wrong. As long as I've made accurate assumptions about what right and wrong are, this is good.
use Brain\Monkey\Functions;
class TestWhatever extends YourTestCase {
public function testInsert(){
//A fake wp_insert_post() that always returns 1
Functions\when('wp_insert_post' )->justReturn(1);
$this->assertIsNumeric(
(new Storage() )->save('Hello Royvan')
);
}
public function testInsertThrows(){
Functions\when('wp_insert_post' )
->justReturn( new \stdClass() );
$this->expectException(\Exception::class);
(new Storage() )->save('Make Errors!');
}
}
These Tests May Not Be Enough
In this post, I've shown how to fake WordPress core functions during unit testing. Don't forget that these tests rely on assumptions. Make sure to confirm your assumptions with integration or acceptance tests.
If you want to get more comfortable with this type of testing, I would suggest making a new plugin from my template, adding a new class that calls functions you may commonly use, like get_post()
orupdate_option()
and write unit tests for them. As you do that, keep in mind what assumptions you're making. If any of them make you uncomfortable, write integration tests instead :)
Cover image: Photo by Benjamin Voros on Unsplash
Posted on November 30, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.