Better functional tests for WordPress in Codeception – 01

Using WordPress code in functional tests.

User voice

Not a long time ago I was lamenting the limited implementation of the WordPress module part of wp-browser.
The inability to tap into WordPress methods directly was a limitation that made testing some WordPress implementations, on a functional level, impossible.
Way before I did, other wp-browser users had noticed and reported the limitation; sometimes with good reasons and arguments and sometimes due to a misunderstanding of how “stuff” should be tested at what level.
Well, the first step in that direction is done with the release of version 1.20 of wp-browser.

A code example

In this example I want to test if, as a WordPress site administrator, I could create posts in the site using the WordPress REST API and its content endpoints.
The functional test I’d write could be this:

/**
 * It should allow an admin to insert a post
 *
 * @test
 */
public function it_should_allow_an_admin_to_insert_a_post(FunctionalTester $I) {
    $I->sendPOST( 'wp-json/wp/v2/posts/', [
        'title'       => 'A post',
        'content' => 'A post content',
    ] );

    $I->seeResponseCodeIs( 201 ); // it means "created"
    $I->seeResponseIsJson();
}

The problem with this test is that the REST API has no way to identify the posting user and will, thus, negate the request and return a 403 (unauthorized) status.
The way the REST API identifies a user is by verifying a nonce created for the current user and the wp_rest action.
The function to create a nonce is wp_create_nonce(), but that function is not available in the scope of the functional test in question.
The latest version of the WPLoader module introduces an option to “just bootstrap WordPress” (understanding the consequences of doing it) in the tests' variable scope.
With reference to wp-browser README I can configure my suite to use the modules I need for the tests:

class_name: FunctionalTester
modules:
    enabled:
        - \Helper\Functional
        - REST
        - WPDb
        - WPBrowser
        - Asserts
        - WPLoader
    config:
        # needed by the REST module
        WPBrowser:
            url: 'http://wp.localhost'
            adminUsername: admin
            adminPassword: admin
            adminUrl: /wp-admin

        # to test the REST API
        REST:
            depends: WPBrowser
            url: 'http://wp.localhost'

        # to manage database insertions and states 
        WPDb:
            dsn: 'mysql:host=localhost;dbname=wp'
            user: root
            password: root
            dump: tests/_data/dump.sql # load this...
            populate: true # ...before the tests...
            cleanup: true # ... and between tests
            url: 'http://wp.localhost'
            tablePrefix: wp_

        # to get access to WordPress code in the tests
        WPLoader:
            # just load WordPress using the same db as WPDb
            loadOnly: true
            wpRootFolder: /tmp/wordpress
            dbName: wp
            dbHost: localhost
            dbUser: root
            dbPassword: root

After a run of codecept build I can update the test to use, and now I can, WordPress functions to create the nonce I need.

/**
 * It should allow an admin to insert a post
 *
 * @test
 */
public function it_should_allow_an_admin_to_insert_a_post(FunctionalTester $I) {

    // create a user that can create posts
    $user_id = $I->haveUserInDatabase( 'user', 'administrator', [ 'user_pass' => 'user' ] );

    // login to get the cookies set
    $I->loginAs( 'user', 'user' );

    // gather the nonce "recipes"
    $_COOKIE[ LOGGED_IN_COOKIE ] = $I->grabCookie( LOGGED_IN_COOKIE );
    wp_set_current_user( $user_id );

    // create the nonce
    $nonce = wp_create_nonce( 'wp_rest' );

    // this is the header the REST API will look up to find the nonce
    // and identify the user together with the cookies
    $I->haveHttpHeader( 'X-WP-Nonce', $nonce );

    $I->sendPOST( 'http://tribe.localhost/wp-json/wp/v2/posts/', [
        'title'       => 'A post',
        'content' => 'A post content',
    ] );

    $I->seeResponseCodeIs( 201 );
    $I->seeResponseIsJson();
}

To avoid repetition, the first part of the test can be moved in a Codeception Step object and reduce the test code even more:

/**
 * It should allow an admin to insert a post
 *
 * @test
 */
public function it_should_allow_an_admin_to_insert_a_post(FunctionalTester $I) {
    $I->setupAuthForA('administrator'); // we do not really care about name, password et cetera

    $I->sendPOST( 'http://tribe.localhost/wp-json/wp/v2/posts/', [
        'title'       => 'A post',
        'content' => 'A post content',
    ] );

    $I->seeResponseCodeIs( 201 );
    $I->seeResponseIsJson();
}

Next

This new development opens some previously closed doors but there is more in the new version of the WPLoader module I will explore in a next post.