If you write automated tests for your PHP code (and you should try!) you’ll often find yourself needing to mock/stub calls to pieces of code that are not under test.

With WordPress, our unit tests tend to be more akin to integration/functional tests where we’re testing that several pieces of interrelated code work well when exercised in the whole. As a result of this, it’s quite common that the code under test may call out to third party services or other parts of the codebase that do not produce deterministic results.

Code needing to mock WP_Http

One example of non-determinism in our code are HTTP requests.

This is because we are unlikely to be in control of the response from the requested URL and this will therefore produce potentially different results on every invocation of the code.

For me, recent experience of this came in the form of the work I did to add a new REST API endpoint within the Gutenberg Plugin which allows a developers to retrieve information from a remote URL (such as the contents of the <title> tag). In order to do this the code makes a HTTP call to the remote URL and processes the response to use within the API endpoint.

As you can imagine, when placing this system under test, the major source of non-determinism was the response returned from the HTTP request to the remote URL. I was making the call using wp_remote_get which is a utility function which delegates its responsibility to the underlying WP_Http API.

Mocking WP_Http responses

To test code like this we need to make it deterministic, which is to say that we need to mock out the response we received from the HTTP request in order to return a known result across all our test runs.

The best way I’ve found to do this is to use the pre_http_request filter which resides within the request method of the WordPress WP_Http class.

As explained in the documentation, this filter allows us to filter the preemptive return value of an HTTP request.

Returning a non-false value from the filter will short-circuit the HTTP request and return early with that value.

https://developer.wordpress.org/reference/hooks/pre_http_request/#description

What this means, is that any non-falsey (and valid) value we return from the filter will short-circuit the execution of the WP_Http request and cause it [the known value] to be returned as the value of the response. This ensures that the actual HTTP request is never made and enables us to return a known value which is entirely under our control.

We can employ this technique within our automated tests to ensure we have deterministic results.

How to mock HTTP requests in WordPress unit tests

Let’s an example an from the REST API endpoint I mentioned above.

public function test_get_items() {

    wp_set_current_user( self::$admin_id );

    $request = new WP_REST_Request( 'GET', '__experimental/url-details' );
    $request->set_query_params(
        array(
            'url' => 'https://google.com' // not in our control
        )
    );
    $response = rest_get_server()->dispatch( $request );
    $data     = $response->get_data();

    $this->assertEquals(
        array(
            'title' => '' // how can we reliably assert here?
        ),
        $data
    );
}

In the example above, we are making a RESTful request to the API endpoint at __experimental/url-details. This endpoint makes a HTTP request to the remote URL passed to the endpoint as the url parameter. Currently, as we are not fully in control of the website at https://google.com there is no way for us to reliably perform an assertion on the expected result of our test run.

In order to achieve this we first need to filter the pre_http_request hook to return known values:

public function test_get_items() {

    // Hook in and return a known response
    add_filter( 'pre_http_request', function() {
        return array(
            'headers'     => array(),
            'cookies'     => array(),
            'filename'    => null,
            'response'    => 200,
            'status_code' => 200,
            'success'     => 1,
            'body'        => file_get_contents( __DIR__ . '/fixtures/example-website.html' ) : '',
        );
    }, 10, 3 );

    // Rest of method here...
}

You will note we can fully control everything about the response including the status_code and the body. In this case we are reading in a simple fixture file which contains a basic HTML webpage to use as our body.

With this in place we can now adjust our test to assert on the known response values:

// Note the <title> comes from the fixture HTML returned by
// the filter `pre_http_request`.
$this->assertEquals(
    array(
        'title' => 'Example Website &mdash; - with encoded content.',
    ),
    $data
);

Putting this all together we have a test which reliably exercises the functionality of the REST API endpoint, without reying on non-deterministic input (eg: a response from remote URL).

public function test_get_items() {

    // Hook in and return a known response
    add_filter( 'pre_http_request', function() {
        return array(
            'headers'     => array(),
            'cookies'     => array(),
            'filename'    => null,
            'response'    => 200,
            'status_code' => 200,
            'success'     => 1,
            'body'        => file_get_contents( __DIR__ . '/fixtures/example-website.html' ) : '',
        );
    }, 10, 3 );

    

    wp_set_current_user( self::$admin_id );

    $request = new WP_REST_Request( 'GET', '__experimental/url-details' );
    $request->set_query_params(
        array(
            'url' => 'https://google.com' // not in our control
        )
    );
    $response = rest_get_server()->dispatch( $request );
    $data     = $response->get_data();

    // Note the <title> comes from the fixture HTML returned by
    // the filter `pre_http_request`.
    $this->assertEquals(
        array(
            'title' => 'Example Website &mdash; - with encoded content.',
        ),
        $data
    );
}

Of course, the code above is largely illustrative. You might find it helpful to see the source code for the tests which run against the real API endpoint.

This technique will allow you to mock out any HTTP requests you make within your unit tests so long as you are using one of the functions that form part of the WP_Http API, including:

…and more. I’ve found it to be extremely handy for writing PHPUnit tests for WordPress code.

If you’ve found this useful, or you have a better technique then let me know in the comments.

One response to “Mocking WP_Http in WordPress PHP Unit tests

  1. Jer Clarke says:

    Thank you for this! Really great explanation of a core principle that I needed to come to grips with in using the ‘wpunit’ testing!

    My particular goal was to test handling of external RSS feeds, i.e, using the core fetch_feed(), but of course I didn’t want to have my tests depend on an external feed.

    Here’s the code I ended up using, in case it helps others (since I couldn’t find anything similar online).

    The code is for the Codeception-based “WP Browser” suite, and I don’t have time to convert it, but it should be pretty easy to use the contents in a vanilla WPUnit test if that’s what you want (though WP Browser is SOOOO much better, I recommend it to everyone).

    The one thing not included below is the actual demo RSS file, which you can create for yourself by loading an RSS feed and pasting the result into a text file at the ‘this->demoFeedFile’ location.

    IMPORTANT DIFFERENCE FOR RSS: The main thing that’s specifically different for RSS, compared to your example above, is the need for the custom header “‘content-type’ => ‘application/rss+xml; charset=UTF-8’,”. Without it, fetch_feed() will fail because SimplePie checks for it while fetching.

    filterPreHttpRequestForExampleFeed() is working properly by using it with the default core function
    $feed = fetch_feed($this->demoFeedUrl);

    $this->assertInstanceOf('SimplePie', $feed);

    $title = $feed->get_title();

    $this->assertSame("Example Site Name", $title);
    }

    public function getDemoFeedItem(int $index) {

    $feed = fetch_feed($this->demoFeedUrl);
    $feed_items = $feed->get_items();

    return $feed_items[$index];
    }

    /**
    * Helper: Filter HTTP requests for our example URL to return our example RSS data
    *
    * Filter reference from WP_Http->request():
    * $pre = apply_filters( 'pre_http_request', false, $parsed_args, $url );
    * @see https://aheadcreative.co.uk/articles/mocking-wp_http-in-wordpress-php-unit-tests/ used for inspiration
    * @param boolean $deprecated Always false
    * @param array $parsed_args
    * @param string $url
    * @return bool|array Bool false (exactly) tells it to proceed, otherwise array of results to return instead of fetching actual URL
    */
    public function filterPreHttpRequestForExampleFeed($deprecated, $parsed_args, $url) {

    if ($url != $this->demoFeedUrl) {
    return false;
    }

    $file_contents = file_get_contents( __DIR__ . $this->demoFeedFile);

    $results = array(
    'headers' => array(
    // Important: SimplePie checks mime type and without this it fails
    'content-type' => 'application/rss+xml; charset=UTF-8',
    ),
    'cookies' => array(),
    'filename' => null,
    'response' => 200,
    'status_code' => 200,
    'success' => 1,
    'body' => $file_contents,
    );

    return $results;
    }

    /**
    * Helper: Filter HTTP requests for our example INVALID URL to return failure
    *
    * Filter reference from WP_Http->request():
    * $pre = apply_filters( 'pre_http_request', false, $parsed_args, $url );
    * @see https://aheadcreative.co.uk/articles/mocking-wp_http-in-wordpress-php-unit-tests/ used for inspiration
    * @param boolean $deprecated Always false
    * @param array $parsed_args
    * @param string $url
    * @return bool|array Bool false (exactly) tells it to proceed, otherwise array of results to return instead of fetching actual URL
    */
    public function filterPreHttpRequestForInvalidFeed($deprecated, $parsed_args, $url) {

    if ($url != $this->invalidFeedUrl) {
    return false;
    }

    // Returning null makes the http request fail
    return null;
    }
    }

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.