WP_Mock and WordPress Unit Testing with PHPUnit
I don't work in PHP too often but needed to write a plugin for a Redis cache project I was working on. It gave me the opportunity to dig into PHPUnit and how to test WordPress related code effectively.
The Goal
I wanted to be able to test the functionality of my plugin. Generally it's not too complicated. Whenever a post is saved, update the Redis cache for that item.
To write the test I ended up working from top to bottom on the function I was testing and determine what different states I wanted to test for.
What about WordPress specific functions
Right away I ran into the issue of how to deal with WordPress specific global functions.
I am using rest_do_request
to get the REST response for a given post. But I don't want to test if rest_do_request
is working, I just want to be able to specify what it's response should be. That's where mocking comes in.
WP_Mock
WP_Mock is a library that helps with mocking WordPress specific functionality. I'll be honest that I don't totally see the benefit to using WP_Mock over a lower level library, but if the few I did see made it worth it.
Under the hood it uses Mockery and just adds an abstraction layer on top.
I also found that enabling Patchwork solved some issues I was having. Again, not totally sure what additional functionality Patchwork adds, the documentation wasn't clear, but it seemed to be needed for what I was doing.
Mocking a class
Even though rest_do_request
was essentially the first thing I needed to mock, it requires a WP_REST_Response
as the parameter. Again, we don't want to test WP_REST_Response
, just be able to tell it what its output should be. And I should note that it's not even available unless we spin up an entire WordPress instance. But that would also defeat the purpose of a unit test and would push closer to an integration or end to end test.
No classing mocking in WP_Mock
I couldn't figure out how to use WP_Mock to mock a class. That's where knowing it was based on mockery helped because I could use it without adding a new dependency.
$WP_REST_Request_Mock = Mockery::mock(WP_REST_Request::class);
$WP_REST_Request_Mock
->shouldReceive('__construct')
->with("GET", "/wp/v2/post/14");
Using the mockery static function mock
we first create the mock. Then we also mock the __construct
method on the class so we can expect what its parameters should be.
Mocking a function
Again, I still am not clear on the benefit of using WP_Mock for this vs straight up mockery, but I still used it and hopefully I'll figure out why its better.
WP_Mock::userFunction('rest_do_request')
->once()
->andReturn($WP_REST_Response);
Now we've mocked rest_do_request
, asserted it should be called just once and then telling it to return a WP_REST_Response
object that was mocked (I'm not showing that here).
Now when we run our function, when it calls rest_do_request
we can control exactly what goes in and out of that function and assert based on that.
Issue with add_action
Because I wanted to keep my plugin simple and just use functions instead of encapsulating in a class, as soon as I required my plugin file, it would run the add_action
function.
Because add_action
is so core to WordPress, I sometimes forget that it's actually a function. This is where WP_Mock has a special way of handling this.
setUpBeforeClass
I decided to run this code in the setUpBeforeClass
lifecycle method that PHPUnit provides. That way we can use WP_Mock::expectActionAdded
.
public static function setUpBeforeClass(): void
{
WP_Mock::expectActionAdded('wp_after_insert_post', "Kaena\Content_Cache\cache_post_on_save", 10, 2);
require_once __DIR__ . "/../cache-post-on-save.php";
}
It's similar to WP_Mock::userFunction
but is specifically made to expect the add_action
function to be run.
I create the mock before we require the function file so that add_action
won't run until we've first mocked it.
What's more
That's about as far as I got with it. I believe there are some custom assertions that WP_Mock
provides but I'd really like to know more of the benefits of using it.
Their documentation is clear and concise, but not very complete. But generally I was pleased with my experience and feel like my code is now covered I'll see if anything breaks.