I’ve written a lot about the WordPress REST API for Torque, but one thing I have not covered is unit testing custom APIs. That is exactly what this article is.
I used a PHP program run the code and compare the results to an expected value. If you want to learn more, I would recommend reading Pippin Williamson’s series on WordPress unit testing. It’s quite excellent and covers how to setup your local environment for testing and how to write tests.
For example here is a very simple test:
<?php function answer(){ return 42; } class Test_42 extends WP_UnitTestCase { public function testAnswer(){ $this->assertEquals( answer(), 42 ); } }
It’s a contrived example, but we can see that it’s using the assertEquals() method to ensure that this function is, in fact, returning 42. Now we have a test to prove why it did what we expected. The point is that assertEquals() compares the result of the function to the expected value I provided and “asserts” they are equal.
Here’s a different example where I have created a CRUD class that can only store numbers less than 42. Then there are two tests to prove that the number 10 can be stored and retrieved and one to prove that the number 100 can not be:
<?php class limited_crud { public static function save( $value ){ if( absint( $value ) < 42 ){ update_option( 'hishawn', absint( $value ) ); }else{ update_option( 'hishawn', 0 ); } } public static function get(){ return get_option( 'hishawn' ); } } class Test_42 extends WP_UnitTestCase { public function testValidInput(){ $value = 10; limited_crud::save( $value ); $this->assertSame( limited_crud::get(), $value ); } public function testInvalidInput(){ $value = 1000; limited_crud::save( $value ); $this->assertSame( limited_crud::get(), 0 ); } }
Notice that I switched from assertEquals() to assertSame(). When using assertEquals() the type and value are checked. In this case, I’m fine with type changing to string from integer. The assertSame() check ignores type. The test assertSame( 10, ’10’ ); would pass, while assertEquals( 10, ’10’ ); would fail.
What To Test
One thing I always advocate when designing custom REST APIs using WordPress, that the callbacks for each endpoint should only handle passing input data to another class or function for the actual functionality of the route, and format the response. Doing this abides by the separation of concerns.
This is something that I show in my course on modern WordPress development with the REST API. It goes over how to design a CRUD API for accessing data in a plugin or site using PHP. Then I show how to wrap that up in custom API endpoints. I’ve covered custom API endpoints before on Torque as well.
This kind of isolation means that each system can be tested separately. If the CRUD system is tested, then the API routes that wrap it can be safely tested assuming that the CRUD system works. This is just like how we can assume that the underlying REST API infrastructure from core works.
So the unit tests for the REST API route will just need to do two things:
- Verify that the routes are added properly
- Verify that the responses are as expected.
Again, we are assuming that everything else in the application is tested, so that’s all we need. Let’s walk through both parts.
Testing That Endpoints Are Added
Before we can test that an endpoint works properly we need to make sure it has been added. This type of test is fairly simple, but it can catch errors that happen when register_rest_route() is used improperly.
It’s important, that when setting up unit tests for the REST API, we create a mock instance of WP_REST_Server. If we don’t we can’t really test the system. In unit tests, the method, setUp() is called before the tests are run. This allows us to set things up first. Here is how I setup my tests to ensure the REST API is properly loaded:
<?php class Test_API extends WP_UnitTestCase{ public function setUp() { parent::setUp(); /** @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; $this->server = $wp_rest_server = new \WP_REST_Server; do_action( 'rest_api_init' ); } }
This populates the global $wp_rest_server and then does the rest_api_init action. Without that, none of our tests would fail, and the failure would be because of bad test design.
Now that that is in place, I like to setup a class property with the name of my route, with its namespace so I can test if it is in the routes added by the REST API. Here is updated code that gives me that:
class Test_API extends WP_UnitTestCase{ /** * Test REST Server * * @var WP_REST_Server */ protected $server; protected $namespaced_route = 'caldera/forms'; public function setUp() { parent::setUp(); /** @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; $this->server = $wp_rest_server = new \WP_REST_Server; do_action( 'rest_api_init' ); } }
Once that’s in place I can test two things using WP_REST_Server::get_routes(). That method returns an array of routes. The array keys are the routes, for example, “wp/v2/posts” and those arrays have the arguments and callbacks for the routes. So, I can test that there is a key of that array for my route and that the contents of that array key are correct:
class Test_API extends WP_UnitTestCase{ /** * Test REST Server * * @var WP_REST_Server */ protected $server; protected $namespaced_route = 'caldera/forms'; public function setUp() { parent::setUp(); /** @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; $this->server = $wp_rest_server = new \WP_REST_Server; do_action( 'rest_api_init' ); } public function test_register_route() { $routes = $this->server->get_routes(); $this->assertArrayHasKey( $this->namespaced_route, $routes ); } public function test_endpoints() { $the_route = $this->namespaced_route; $routes = $this->server->get_routes(); foreach( $routes as $route => $route_config ) { if( 0 === strpos( $the_route, $route ) ) { $this->assertTrue( is_array( $route_config ) ); foreach( $route_config as $i => $endpoint ) { $this->assertArrayHasKey( 'callback', $endpoint ); $this->assertArrayHasKey( 0, $endpoint[ 'callback' ], get_class( $this ) ); $this->assertArrayHasKey( 1, $endpoint[ 'callback' ], get_class( $this ) ); $this->assertTrue( is_callable( array( $endpoint[ 'callback' ][0], $endpoint[ 'callback' ][1] ) ) ); } } } } }
This test, which can easily be adapted to work with multiple routes by extending it and changing the value of the namespaced_route property, ensures that routes are added properly. That’s good, especially as it prevents future changes from accidentally removing those routes.
Testing Route Responses
The other thing I said we needed to do was test that the API could generate the right responses. When I first thought about creating these types of tests, I thought I would have to make an HTTP request to the route and then test the response. However, that’s too complicated.
Instead, I assume that the WordPress REST API infrastructure works, and create a mock request and check its response. This is how the tests for the default endpoints works. Reading their source is how I figured it out. For example, look at this test, copied from a test of the default post endpoint:
<?php //copied from: https://github.com/WP-API/WP-API/blob/develop/tests/test-rest-posts-controller.php#L104-L128 Much GPL public function test_get_items_author_query() { $this->factory->post->create( array( 'post_author' => $this->editor_id ) ); $this->factory->post->create( array( 'post_author' => $this->author_id ) ); // All 3 posts $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); $response = $this->server->dispatch( $request ); $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( 3, count( $response->get_data() ) ); // 2 of 3 posts $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); $request->set_param( 'author', array( $this->editor_id, $this->author_id ) ); $response = $this->server->dispatch( $request ); $this->assertEquals( 200, $response->get_status() ); $data = $response->get_data(); $this->assertEquals( 2, count( $data ) ); $this->assertEqualSets( array( $this->editor_id, $this->author_id ), wp_list_pluck( $data, 'author' ) ); // 1 of 3 posts $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); $request->set_param( 'author', $this->editor_id ); $response = $this->server->dispatch( $request ); $this->assertEquals( 200, $response->get_status() ); $data = $response->get_data(); $this->assertEquals( 1, count( $data ) ); $this->assertEquals( $this->editor_id, $data[0]['author'] ); }
This code covers testing a bunch of different scenarios. Each time a request is created with a new instance of the WP_Rest_Request() class and dispatching it using WP_Rest_Server to create a response that can be verified. This is a great example of how you can get posts in the format of a REST API response without making a loop back request.
Let’s put this to use to test a route. Let’s assume we have a route that takes an ID at the end of the URL and return that item’s name. We would want a test like this:
class Test_API extends WP_UnitTestCase { /** * Test REST Server * * @var WP_REST_Server */ protected $server; protected $namespaced_route = 'caldera/forms'; public function setUp() { parent::setUp(); /** @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; $this->server = $wp_rest_server = new \WP_REST_Server; do_action( 'rest_api_init' ); } public function test_name_route() { $request = new WP_REST_Request( 'GET', '/api/v2/name' ); $response = $this->server->dispatch( $request ); $this->assertEquals( 200, $response->get_status() ); $data = $response->get_data(); $this->assertArrayHasKey( 'name', $data ); $this->assertEquals( 'shawn', $data[ 'name' ] ); } }
What I’m testing here is that my request returned a 200 status code, that it created an array with the key name, and that array key’s value was correct. Whatever internal logic made this happen, I’m not testing. I should test it elsewhere, but not in this test.
4 Comments