This post is part of a series of posts on extending the WordPress REST API using advanced object-oriented PHP. In the first post, I showed you how to use three filters to modify the schema of any post type route. Using an object-oriented approach is more complex, and takes more work than using functional programming styles or using some object-oriented code but not following the SOLID principles closely as I am attempting to do in these posts.
That code we’ve been working with uses three WordPress filters and interacts with multiple WordPress APIs. The goal of these posts is to show you why following these established best practices leads to code that is more complex, but is not overly complex while being highly maintainable and extensible.
Most of this series is about automated testing. We’ll find that writing testable code helps us reach these goals. While doing so, we get test coverage and the ability to use continuous integration and continuous deployment tools.
In my last post, I covered setting up a WordPress plugin for unit tests and integration tests. In this post, we’ll refactor the code from the first post, so that it is testable in isolation from WordPress. Then we’ll write some unit tests to prove that. These tests will not cover the whole system, that will require integration tests, which we’ll get to in the next post in this series.
Refactoring For Testability
In my last post, I showed a class that will cause WP_Query not to query the WordPress database. Instead, this class provides its own array of WP_Posts. This pattern, replacing WP_Query with an alternative API is especially useful for solving search and scalability issues in WordPress.
<?php /** * Class FilterWPQuery * * Changes WP_Query object * * @package ExamplePlugin */ class FilterWPQuery { /** * Demonstrates how to use a different way to set the posts that WP_Query returns * @uses "posts_pre_query" * * @param $postsOrNull * @param WP_Query $query * @return mixed */ public static function posts_pre_query($postsOrNull, $query) { //Only run during WordPress API requests if (defined('REST_REQUEST') && REST_REQUEST) { //Prevent recursions remove_filter('posts_pre_query', [FilterWPQuery::class, 'posts_pre_query'], 10); //Don't run if posts are already sent if (is_null($postsOrNull)) { //Create 4 mock posts with different titles $mockPosts = []; for ($i = 0; $i <= 3; $i++) { $mockPosts[$i] = (new WP_Post((new stdClass()))); $mockPosts[$i]->post_title = "Mock Post $i"; $mockPosts[$i]->filter = "raw"; } //Return a mock array of mock posts return $mockPosts; } //Always return something, even if its unchanged return $postsOrNull; } } }
In my last post, I made it clear that is class was not yet ready for unit testing. That method has four concerns:
- The callback for the fitler
- Determining if the filter should run
- Getting the new results
- Providing a way to remove the filter.
Let’s let the single responsibility principle be our guide and break this class up into four methods. This will allow us to separate the business logic that we want to unit tests from integrations with WordPress, which we can add integration tests for later. Doing so will allow us to create a testing mock.
These four concerns are the public API of the class. Public APIs get defined in PHP interfaces. By doing so, we can use type hints and return type declarations for this interface. That way how the class implementing that interface works will not matter to other classes that rely on it.
This approach, which is a key part of the SOLID principles, does not just help us build our testing mock. It means that conceptually we are no longer building a system for replacing the results of WP_Query during REST API requests. Instead, we a are building a system for replacing the results of WP_Query, in any arbitrary situation, with a reference implementation that replaces WP_Query during a REST API requests.
This still meets our original requirements. Also, it’s going to make adding other implementations in the future much easier.
Here is the interface, with one method per concern:
interface FiltersPreWPQuery { /** * Change the results of WP_Query objects * * @uses "posts_pre_query" * * @param $postsOrNull * @return WP_Post[] */ public static function callback($postsOrNull); /** * Should this request be filtered? * * @return bool */ public static function shouldFilter() :bool; /** * Remove the filter using this callback * * @return void */ public static function removeFilter(); /** * Create the array of posts to return * * @return WP_Post[] */ public static function getPosts() :array; }
Now that we have an interface in place, we will need to confirm the original class to the new standard. It’s the same thing, just in four methods instead of one:
class FilterWPQuery implements FiltersPreWPQuery { /** * Demonstrates how to use a different way to set the posts that WP_Query returns * * @uses "posts_pre_query" * * @param $postsOrNull * @return WP_Post[] */ public static function callback($postsOrNull) { //Only run during WordPress API requests if (static::shouldFilter()) { //Prevent recursions //Don't run if posts are already sent if (is_null($postsOrNull)) { //Get mock data $postsOrNull = static ::getPosts(); } //Always return something, even if its unchanged return $postsOrNull; } } /** @inheritdoc */ public static function shouldFilter() :bool { return defined('REST_REQUEST') && REST_REQUEST; } /** @inheritdoc */ public static function removeFilter() { remove_filter('posts_pre_query', [__CLASS__, 'posts_pre_query'], 10); } /** @inheritdoc */ public static function getPosts() : array { //Create 4 mock posts with different titles $mockPosts = []; for ($i = 0; $i <= 3; $i++) { $mockPosts[$i] = (new WP_Post((new stdClass()))); $mockPosts[$i]->post_title = "Mock Post $i"; } //Return a mock array of mock posts return $mockPosts; } }
This is more complex than it was before. Adding complexity, by itself, is bad. We need a reason that the added code complexity, which is a technical debt, is worth it. Let’s look at why it is.
In our list of the concerns, some of the concerns involved interactions within WordPress. We can divide those into interactions that a simple mock can solve, and those that we need to test the effects of. For example, I can create a mock of WP_Post, which we’ll need, that I trust, just by adding this to my Tests/Mocks:
<?php namespace CalderaLearnRestSearchTestsMock; /** * Class FilterWPQuery * * Mock class that is totally decoupled from WordPress * * @package CalderaLearnRestSearchTestsMock */ class FilterWPQuery extends CalderaLearnRestSearchFilterWPQuery { /** @inheritdoc */ public static function shouldFilter() :bool { return true; } /** @inheritdoc */ public static function removeFilter() { return; } /** @inheritdoc */ public static function getPosts() : array { //Create 4 mock posts with different titles $mockPosts = []; for ($i = 0; $i <= 3; $i++) { $mockPosts[$i] = (new WP_Post((new stdClass()))); $mockPosts[$i]->post_title = "Mock Post $i"; } //Return a mock array of mock posts return $mockPosts; } }
Note that I had to keep this in the global namespace because in this instance, it’s WP_Posts. In order to do that and still use composer’s autoloader, I added this file to the “files” argument of the development autoloader. I also told PHPCS to ignore most of this file, as it’s not following a lot of the code formatting rules, but I care more about getting the tests right than I do about code formatting for a mock WP_Post object.
Great, problem solved. In our mock FilterWPQuery, we’ll use those mock WP_Posts. In mock FilterWPQuery I’m going to solve the problems of interactions that have side effects, such as removing the filter, by ignoring them.
That’s why we have integration tests. The interface enforces the same pattern, we can trust as long as it’s being used, our integration tests will cover the methods we can’t totally cover in the unit tests. Here is the mock FilterWPQuery:
<?php namespace CalderaLearnRestSearchTestsMock; /** * Class FilterWPQuery * * Mock class that is totally decoupled from WordPress * * @package CalderaLearnRestSearchTestsMock */ class FilterWPQuery extends CalderaLearnRestSearchFilterWPQuery { /** @inheritdoc */ public static function shouldFilter() :bool { return true; } /** @inheritdoc */ public static function removeFilter() { return; } /** @inheritdoc */ public static function getPosts() : array { //Create 4 mock posts with different titles $mockPosts = []; for ($i = 0; $i <= 3; $i++) { $mockPosts[$i] = (new WP_Post((new stdClass()))); $mockPosts[$i]->post_title = "Mock Post $i"; } //Return a mock array of mock posts return $mockPosts; } }
In this mock, we solve the problem that the method shouldFilter was relying on a constant set in WordPress by just returning true. If we add another mock that returns false on this method, we can fully test this code in isolation. Our integration tests will just cover that when used with WordPress, this code reacts to that effect correctly.
Unit Testing Hook Callbacks In Isolation
Now, we’re going to test the callback for a WordPress hook, in total isolation. This is an easy one to do as the filter we’re using, posts_pre_query, is an early-return filter. It is null by default and causes the query method of WP_query to return earlier than would be the default if the return value of posts_pre_query is not null.
This means we only have to cover when the callback is null and when it is not null. Other hooks are more complex. I chose one with less complexity because it’s easier to teach that way.
Since callback is the one method that uses the other three methods, we want to write tests for those other three methods first. The method callback has the other three methods as its only dependencies. We can only trust the unit test that covers callback if the other methods are covered, so let’s start with them.
First, let’s look at the getPosts method. For the purposes of this test, I’m only concerned that this method returns an array and that the array has WP_Post objects in it. The contents don’t matter.
<?php /** * Test that the getPosts method return an array * * @covers CalderaLearnRestSearchFilterWPQuery::getPosts() */ public function testGetPosts() { //Get the mock posts $results = FilterWPQuery::getPosts(); //Make sure results are an array $this->assertTrue(is_array($results)); }
This just tests that it is returning an array. It has to return an array, that’s its type hint. This test basically just covers our mocking, not the actual class we want to test. The next test will cover the contents of the array:
<?php /** * Test that the getPosts method return of WP_Posts * * @covers CalderaLearnRestSearchFilterWPQuery::getPosts() */ public function testGetPostsArePosts() { //Get the mock posts $results = FilterWPQuery::getPosts(); $this->assertFalse(empty($results)); //Make sure results are an array of WP_Posts $looped = false; foreach ($results as $result) { $looped = true; //Make sure all results are WP_Posts $this->assertTrue(is_a($result, 'WP_Post'), get_class($result)); } //Make sure loop ran $this->assertTrue($looped); }
Again, this is more so covering the mock rather than the actual test. It’s useful to establish a pattern for testing, that we can repeat in each practical application of this code to establish full coverage. For now, it’s an important start.
By the way, I’ve been saying “a test covers a method” to talk about the method in a test suite that encapsulates one or more tests that prove the functionality of the method being covered. We can document this link with an @covers phpdock annotation.
Doing so makes our code more readable. I’ll also come back to @covers annotations in a future post on creating code coverage reports. These reports help us determine what percentage of our codebase is tested.
In the tests I’ve shown so far, I used fully qualified namespaces for the @covers annotations. That was because FilterWPQuery is in scope as the mock. For my next test, which is covering the shouldFilter() method, this mock really doesn’t tell me anything about my actual code, so I’m not going to claim it does. This would cause a coverage report that isn’t 100% for that class. I can attain that full coverage with unit tests. Coverage reports are only useful if they are honest. Instead, I’m going to say that it covers the mock’s shouldFilter() method and the next test I’m about to write, testCallbackWithNull. I’m doing that to remind myself why I am leaving in this almost totally pointless test: the next test relies on this mock method working the way that test asserts it does.
<?php /** * Test that our mock returns true for should filter * * @covers FilterWPQuery::shouldFilter() * @covers FilterWPQueryTest::testWithNull() */ public function testShouldFilter() { $this->assertTrue(FilterWPQuery::shouldFilter()); }
Now, let’s actually test the callback. We can trust that the other methods we need work. This is important as the first thing I need is something to compare the results of the callback method to. The mock object I’m working with can provide exactly the correct mock data I need, the tests I just showed prove that.
I now can use getPosts and callback to create two variables, that should be the same. Here is how I begin:
<?php //Use the mock data we have in our mock class as the expected values $expected = FilterWPQuery::getPosts(); //Get the results from the callback $results = FilterWPQuery::callback(null);
Now I need to make sure that these two variables are the same size, and have the same contents:
<?php /** * Test the result data is consistent * * @covers CalderaLearnRestSearchFilterWPQuery::callback() */ public function testCallbackWithNull() { //Use the mock data we have in our mock class as the expected values $expected = FilterWPQuery::getPosts(); //Get the results from the callback $results = FilterWPQuery::callback(null); //Make sure results are an array $this->assertTrue(is_array($results)); //Make sure the two arrays are the same size $this->assertCount(count($expected), $results); /** These arrays are not the same, compare the meaning of the contents */ //Used to make sure this loop of tests ran $looped = false; /** * Loop through expected, comparing to eactual results * @var int $i * @var WP_Post $post */ foreach ($expected as $i => $post) { $looped = true; //Test that the mock and resulting post titles are the same $this->assertSame($post->post_title, $results[$i]->post_title); } //Make sure loop ran $this->assertTrue($looped); }
Now I can trust that this class works as designed, given the input of null. That is not sufficient as I need to make sure that when it’s passed an array, it does not change that array. The tests looks almost identical. The only difference is I’m passing an array of posts to the method callback.
<?php /** * Test the result data is not changed, when passed an array * * * @covers CalderaLearnRestSearchFilterWPQuery::callback() */ public function testCallbackWithArray() { //Use the mock data we have in our mock class as the expected values $expected = FilterWPQuery::getPosts(); //Get the results from the callback $results = FilterWPQuery::callback($expected); //Make sure results are an array $this->assertTrue(is_array($results)); //Make sure the two arrays are the same size $this->assertCount(count($expected), $results); /** These arrays are not the same, compare the meaning of the contents */ //Used to make sure this loop of tests ran $looped = false; /** * Loop through expected, comparing to eactual results * @var int $i * @var WP_Post $post */ foreach ($expected as $i => $post) { $looped = true; //Test that the mock and resulting post titles are the same $this->assertSame($post->post_title, $results[$i]->post_title); } //Make sure loop ran $this->assertTrue($looped); }
The Really Important Part About Mock Objects
As I was describing the tests for FilterWPQuery, I kept mentioning the deficiencies of my mock. But I want to point out why I’m OK with that, beyond the fact that I’ll add integration and acceptance tests later. If you look at the mock itself, it is not just implementing the interface I created. It’s doing so by extending the original FilterWPQuery and replacing every method, except the callback method.
The unit tests for the method callback cover the real FilterWPQuery::callback(). As long as the other methods of that class do what they are supposed to do, we can trust this test. Mocking allows us to write and trust our tests this way.
Using an interface and an abstract class was added complexity. But as a result, it’s almost impossible to write an implementation of this system that would fail because of FilterWPQuery.
I can’t stress enough that this is the what makes the complexity of SOLID OOP PHP worth it. Instead of writing code to do one thing, we can build systems to do similar things and trust that the pattern always works when applied the only way it can be applied.
Next Steps: Complete Coverage
That’s the basics of unit testing WordPress code, isolated from WordPress. It’s a problem that was solved using mocking and following a principle that requires multiple acronyms to fit in one sentence. I hope this article showed you how to refactor your code so it can be isolated for testing and how to write those tests.
I have combined all of the code examples from this article and the last one into one plugin. I put a git tag where this article leaves off. I’d recommend, to practice what you’ve learned, forking the plugin and adding full test coverage. You will need to do a little refactoring.
I structured the mock object we used for testing to remove the WordPress plugins API. That was fine because we were not doing much with it. For something more complex, like a class that actually added these hooks, mocking the plugins API would be important and a tool such as 10up/wp-mock, would be very helpful.
In my next post, we’ll start testing with less isolation. We’ll be writing integration tests, using WordPress test suite to cover interactions with the WordPress Plugins API.
The post Advanced OOP For WordPress Part 3: Unit Testing For WordPress REST API Plugins appeared first on Torque.