Skip to main content
Adzbyte
TutorialsWordPress

Test a Custom WordPress REST API Endpoint

Adrian Saycon
Adrian Saycon
September 25, 20265 min read
Test a Custom WordPress REST API Endpoint

A useful test suite for a custom WordPress REST API endpoint proves more than a 200 response. It verifies route registration, argument schemas, authorization for each role, validation failures, status codes, response shape, side effects, and the absence of private data. This tutorial tests a small endpoint that marks a post as featured. The examples use WordPress’s REST server directly, so they run quickly without starting a browser or external HTTP server. The suite therefore treats the endpoint as a security and compatibility boundary.

Build an endpoint with a visible contract

Register routes on rest_api_init, include a permission callback, and describe input with an argument schema.

add_action( 'rest_api_init', function () { register_rest_route( 'adz/v1', '/posts/(?P<id>[d]+)/featured', array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => 'adz_set_post_featured', 'permission_callback' => function ( WP_REST_Request $request ) { return current_user_can( 'edit_post', (int) $request['id'] ); }, 'args' => array( 'id' => array( 'type' => 'integer', 'minimum' => 1 ), 'featured' => array( 'required' => true, 'type' => 'boolean' ) ) ) ); } );

The callback must confirm the post type, update metadata, and return explicit fields. This endpoint stores a Boolean as 1 or 0 so the response contract remains independent of metadata storage details.

function adz_set_post_featured( WP_REST_Request $request ) { $post_id = (int) $request['id']; if ( 'post' !== get_post_type( $post_id ) ) { return new WP_Error( 'adz_post_not_found', __( 'Post not found.', 'adz-api' ), array( 'status' => 404 ) ); } $featured = (bool) $request['featured']; update_post_meta( $post_id, 'featured', $featured ? '1' : '0' ); return rest_ensure_response( array( 'id' => $post_id, 'featured' => $featured ) ); }

Do not return an entire post object merely because it is convenient.

Prepare an isolated REST test case

Extend WP_UnitTestCase, create only the fixtures each test needs, and initialize the REST server.

class Adz_REST_Featured_Test extends WP_UnitTestCase { private $server; private $editor_id; private $post_id; public function set_up() { parent::set_up(); global $wp_rest_server; $wp_rest_server = new WP_REST_Server(); $this->server = $wp_rest_server; do_action( 'rest_api_init' ); $this->editor_id = self::factory()->user->create( array( 'role' => 'editor' ) ); $this->post_id = self::factory()->post->create( array( 'post_type' => 'post', 'post_status' => 'publish' ) ); } }

Reset the current user during teardown if later tests could inherit it. Test isolation prevents order-dependent permission results.

Assert the successful request completely

public function test_editor_can_feature_post() { wp_set_current_user( $this->editor_id ); $request = new WP_REST_Request( 'POST', '/adz/v1/posts/' . $this->post_id . '/featured' ); $request->set_param( 'featured', true ); $response = $this->server->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); $this->assertSame( array( 'id' => $this->post_id, 'featured' => true ), $response->get_data() ); $this->assertSame( '1', get_post_meta( $this->post_id, 'featured', true ) ); }

Check both the response and stored state. A callback can accidentally return success while failing to persist, or persist correctly while exposing the wrong representation.

Prove unauthorized users cannot change data

Run separate tests for logged-out visitors and roles without the required capability. Capture the metadata before dispatch and assert that it remains unchanged afterward.

public function test_subscriber_cannot_feature_post() { $subscriber = self::factory()->user->create( array( 'role' => 'subscriber' ) ); wp_set_current_user( $subscriber ); $request = new WP_REST_Request( 'POST', '/adz/v1/posts/' . $this->post_id . '/featured' ); $request->set_param( 'featured', true ); $response = $this->server->dispatch( $request ); $this->assertSame( 403, $response->get_status() ); $this->assertSame( '', get_post_meta( $this->post_id, 'featured', true ) ); }

A permission failure must happen before the callback changes state. Testing the unchanged record makes that guarantee explicit.

Exercise schema and validation failures

Send requests with the required field missing, a string where a Boolean is expected, an ID of zero, a nonexistent record, and a record of the wrong post type. Assert distinct, useful status codes: schema failures typically produce 400 responses, missing resources 404, and forbidden resources 403.

Do not write assertions against complete translated error messages unless wording is part of your public contract. Error codes and status values are more stable and more useful to API clients.

Test method and route boundaries

  • Dispatch GET and DELETE requests and confirm they are not accepted.
  • Call a neighboring namespace and confirm no route matches.
  • Test a trailing slash if clients may send one.
  • Confirm another post ID cannot be substituted to cross an ownership boundary.
  • Test repeated identical requests and decide whether idempotency is expected.

These cases expose accidental method widening and authorization rules that check a general capability but ignore the requested resource.

Inspect the response for data leaks

Assert the exact response keys or use a schema assertion. Ensure internal metadata, author emails, filesystem paths, tokens, debug messages, and private post fields never appear. On failures, verify that exception details are logged for operators but not returned to clients.

If the endpoint uses _links or embeds resources, test those relationships too. A narrow top-level payload can still expose data through linked controllers.

Run the suite in a compatibility matrix

REST behavior depends on WordPress and PHP versions, plugin integrations, and database setup. Run the focused tests on every pull request, then cover the oldest and newest supported environments in CI. A failed security test should block release just as a syntax error would.

Also test conditional requests and caching when the route supports reads. Verify that personalized or permission-sensitive responses are not publicly cached, and that cache keys vary on every dimension that changes the representation. For mutation routes, confirm successful responses do not invite intermediary caching. These behaviors may require an end-to-end HTTP test because the direct REST server does not reproduce every web-server or CDN header.

Test the boundary clients actually use

Direct REST server tests are fast enough to cover roles, schemas, methods, and edge cases in detail. Add a smaller number of end-to-end requests when authentication infrastructure, web-server headers, or proxies matter. WordPress’s custom endpoint documentation defines the route contract. A strong suite proves that the implementation honors it for valid users, invalid input, and hostile requests alike.

Photo by Daniil Komov on Pexels.

Adrian Saycon

Written by

Adrian Saycon

A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.

Discussion (0)

Sign in to join the discussion

No comments yet. Be the first to share your thoughts.

Latest Articles

From the Blog

View all articles