Better functional tests for WordPress in Codeception – 02

Some example usages of the new possibilities offered by the module combinations.

The test bed

The code I’m showing in this post is applied to the REST calculator plugin code.
The plugin ridiculous purpose is to expose a REST API users can use to resolve elementary mathematical operations.
Of course the plugin is completely useless but for its inner capacity to showcase interesting testing related “gotchas”.

Divisions are for registered users only

The plugin currently allows anyone to hit a path like /wp-json/calc/add/1/2 on the site and get a response of 3.
I know because there is a functional test in place (and others really) to check for that (file here):

<?php
namespace Tests\Functional;
class AddRequestCest {
    /**
     * It should store last operation result
     * @test
     *
     * @example [5,4]
     * @example [5,0]
     * @example [5,-5]
     * @example [5,-8]
     * @example [0,0]
     * @example [-1,-3]
     */
    public function return_the_correct_result( \FunctionalTester $I, \Codeception\Example $example ) {
        $first  = $example[0];
        $second = $example[1];

        $I->amOnPage( "/wp-json/calc/add/{$first}/{$second}" );

        $I->seeResponseCodeIs( 200 );
        $I->seeOptionInDatabase( [ 'option_name' => '_transient_last_operation' ] );
    }
}

And now I want to add support for a second operation, the division, reserving this, but, to registered users only.
It’s easy to add a second endpoint in the plugin main file that will handle requests sent to /wp-json/calc/times/4/2; see the code here.
I add a functional test to make sure users that are not subscribers will be negated access in the tests/functional/TimesRequestCest.php file:

<?php
namespace Test\Functional;
use \FunctionalTester;

class TimesRequestCest
{
    /**
     * It should return 403 if user is not subscriber
     *
     * @test
     */
    public function it_should_return_403_if_user_is_not_subscriber(FunctionalTester $I) {
        $I->amOnPage( "/wp-json/calc/times/4/2" );

        $I->seeResponseCodeIs( 403 );
    }
}

The test will pass:

Time to write, now a passing test.

Factories in functional tests

The WordPress REST API supports a number of authentication methods, but the most common one is the one based on cookies.
For a user to be authenticated during a REST API request two things are needed:

  1. a valid nonce for the user and wp_rest action couple in the request X-WP-Nonce header (or _wpnonce request parameter)
  2. a valid login cookie set in the user browser

I’ve shown code to do this in my previous post but I’m showing another take on the same scenario focusing the attention, this time, on another possibility offered by the WPLoader module loadOnly use:

/**
 * It should return the result when the user is a subscriber
 *
 * @test
 */
public function it_should_return_the_result_when_the_user_is_a_subscriber(FunctionalTester $I) {
    // create a subscriber user 
    $userDetails = ['user_login' => 'user', 'role' => 'subscriber', 'user_pass' => 'secret'];
    $id = $I->factory()->user->create($userDetails);

    $I->loginAs('user', 'secret');
    $I->extractCookie(LOGGED_IN_COOKIE);
    wp_set_current_user($id);

    $I->haveHttpHeader('X-WP-Nonce', wp_create_nonce('wp_rest'));
    $I->amOnPage("/wp-json/calc/times/4/2");

    $I->seeResponseCodeIs(200);
    $I->see('2');
}

In the code above I’m accessing the user factory as I would do in integration or WordPress unit tests.
Factories are a functionality offered by the WordPress automated testing suite, and wp-browser by extension, in integration and unit tests.
While this is a functional test the updated WPLoader module will take care, if the loadOnly parameter is set to true, to bootstrap the factories as well.
The factories are available on the factory method of the Tester class; similarly to integration and WordPress unit tests the available factories allow creating posts, pages, users, terms, blogs and all the other WordPress managed entities with little effort.
Since the WPLoader module is set to persist on the same database used by the other modules the WordPress methods and factories all will have a lasting effect on the WordPress environment under test.
The functional suite configuration file has been updated to set up the WPLoader module.

Using plugin defined functions

Similarly to how WordPress functions and classes are made available, in the context of the tests, by the WPLoader function, functions defined by any other WordPress component are.
If a plugin, or theme, is loaded in the initial database dump used to scaffold the initial state of the functional tests, then the functions and classes it defines will be available in the tests too.
To try this out I’m adding a function to the rest-calculator plugin that will take care of “hashing” an input string hitting the /wp-json/calc/hash/<some value> endpoint.
The return value will be the same provided the same input every time. When testing the endpoints on a functional level I do not really care what that value is or how it is built; what I, instead, care about is that the value returned to the user is the exact same the underlying function produces.
To test for this “exact same” value the easiest thing to do is to use the exact same function the plugin uses to generate the hash: the restCalculator_hash one:

function restCalculator_hash($value) {
    return md5(ucwords(str_replace('a', '4', $value)));
}

Writing the functional test is now easier done than said:

<?php


class HashCest {

    /**
     * It should return the correct hash
     *
     * @test
     */
    public function it_should_return_the_correct_hash(FunctionalTester $I) {
        $I->amOnPage("/wp-json/calc/hash/aba");

        $I->seeResponseCodeIs(200);

        $I->see(restCalculator_hash('aba'));
    }
}

Now the sky is the limit for functional tests.
Except…

Isolation still applies

While the new feature allows for more powerful and less redundant testing there are still limits to what can be done in the context of a functional test thanks to the WPLoader module.
The companion modules used to make requests to the the WordPress installation are, usually, WPBrowser, WPWebDriver or the WordPress one.
The first two will call the WordPress server while the latter will call the WordPress installation routing components (files like index.php and /wp-admin/index.php) to resolve the requests.
Each request handling will happen in a separate process that will not share any relation with the one running the tests.
I add a filter to the restCalculator_hash function output and want to test it in a functional test; the proper context for this test would be an integration test yet I will concede for the sake of argument:

/**
 * It should return the filtered hash value
 *
 * @test
 */
public function it_should_return_the_filtered_hash_value(FunctionalTester $I) {
    add_filter('restCalculator_hash', function($value){
        return $value . 'foo';
    });

    $I->amOnPage("/wp-json/calc/hash/aba");

    $I->seeResponseCodeIs(200);

    $I->see(restCalculator_hash('aba'));
}

The test will, but, fail:

The reason is simple: the hash computation returned by the response is not happening in the same process as the test hence the filtering does not apply.
The reason for this is to avoid WordPress “polluting” the global scope with constants and globals that would create an impossible to reset testing environment.

The code

The code shown in the post is available on GitHub.

I appreciate your input