The path to real WordPress functional testing - 01

Making functional testing a reality. Finally.

I get back to this every now and then

I’ve fell with Codeception after watching a video on Laracasts (this one is not it but Jeffrey Way is always worth looking and listening) and taking a look at the testing examples.
As I read through the documentation of Codeception I was excited about how it seamlessly integrated multiple levels of testing in one coherent package and how it made testing… simple, and logic.
When I started working on wp-browser I had the naïve idea that integrating WordPress and Codeception would have been easy; while that idea is no more it became, over time, easier (not easy, though) and my main open-source project.
One technical difficulty, though, kept bugging me and this simple code example seemed like an unreachable line for wp-browser.
While I can tolerate the words of caution on the page not having the same possibility in WordPress frustrated me:

Functional tests are usually much faster than acceptance tests. But functional tests are less stable as they run Codeception and the application in one environment. If your application was not designed to run in long lived processes (e.g. if you use the exit operator or global variables), then functional tests are probably not for you.

Understanding this limit and working around it I’m updating the WordPress module available in wp-browser to allow real functional testing in WordPress.

What could I do with this?

While I’m still working out the limits of this new features this is code I could write in the context of a functional test proper:

use FunctionalTester;

class FunctionalCest {
    /**
     * Test theme footer content can be filtered
     */
    public function test_theme_content_can_be_filtered(FunctionalTester $I) {
        add_filter('my_theme_footer_elements', function () {
            return ['social', 'credits'];
        });

        $I->amOnPage("/");

        $I->seeElement('#my-theme-footer');
        $I->seeElement('#my-theme-footer .social');
        $I->seeElement('#my-theme-footer .credits');

        $postID = $I->havePostInDatabase();

        $I->amOnPage("/index.php?p={$postID}");

        // should never show on singles!
        $I->dontSeeElement('#my-theme-footer');
    }

    /**
     * Test theme footer content will not show if filtered empty
     */
    public function test_theme_footer_content_will_not_show_if_filtered_empty(FunctionalTester $I) {
        add_filter('my_theme_footer_elements', function () {
            return [];
        });

        $I->amOnPage("/");

        $I->dontSeeElement('#my-theme-footer');

        $postID = $I->havePostInDatabase();

        $I->amOnPage("/index.php?p={$postID}");

        // should never show on singles!
        $I->dontSeeElement('#my-theme-footer');
    }
}

What is new and different here is that I’m filtering the my_theme_footer_elements filter that will be used to render some footer elements on the page; for the tests to work as intended the call to the corresponding apply_filters( 'my_theme_footer_elements') function has to happen in the tests scope.
The difference from the current possibility is evident when compared to the closest code I can write, to do the same, with what is available in wp-browser now:

use FunctionalTester;

class FunctionalCest {
    /**
     * Test theme footer content can be filtered
     */
    public function test_theme_content_can_be_filtered(FunctionalTester $I) {
        $code = <<< PHP
add_filter('my_theme_footer_elements', function () {
    return ['social', 'credits'];
});
PHP;

        $I->haveMuPlugin('filter.php', $code);

        $I->amOnPage("/");

        $I->seeElement('#my-theme-footer');
        $I->seeElement('#my-theme-footer .social');
        $I->seeElement('#my-theme-footer .credits');

        $postID = $I->havePostInDatabase();

        $I->amOnPage("/index.php?p={$postID}");

        // should never show on singles!
        $I->dontSeeElement('#my-theme-footer');
    }

    /**
     * Test theme footer content will not show if filtered empty
     */
    public function test_theme_footer_content_will_not_show_if_filtered_empty(FunctionalTester $I) {
        $code = <<< PHP
add_filter('my_theme_footer_elements', function () {
    return [];
});
PHP;

        $I->haveMuPlugin('filter.php', $code);

        $I->amOnPage("/");

        $I->dontSeeElement('#my-theme-footer');

        $postID = $I->havePostInDatabase();

        $I->amOnPage("/index.php?p={$postID}");

        // should never show on singles!
        $I->dontSeeElement('#my-theme-footer');
    }
}

The difference is not dramatic, I’m leveraging the WPFilesystem module and its haveMuPlugin method here, yet the first solution provides a fundamental advantage: I can set break points in the filtering code while testing the code end-to-end (request, handling, response).

How is this achieved and what will be the probable limits?

While I’m still very much working on the feature implementation some of the basics are in place already: I will modify the WordPress module to accept an optional parameter, insulated:

# file tests/functional.suite.yml 

actor: FunctionalTester
modules:
  enabled:
    - WPDb
    - Asserts
    - WPFilesystem
    - WordPress
    - \Helper\Functional
  config:
    WPDb:
      dsn: 'mysql:host=%DB_HOST%;dbname=%DB_NAME%'
      user: %DB_USER%
      password: %DB_PASSWORD%
      dump: 'tests/_data/dump.sql'
      populate: true #import the dump before the tests
      cleanup: true #import the dump between tests
      url: '%WP_URL%'
      urlReplacement: true #replace the hardcoded dump URL with the one above
      tablePrefix: 'wp_'
    WordPress:
      depends: WPDb
      insulated: false # whether requests should happen in a separate process or not
      wpRootFolder: '%WP_ROOT_FOLDER%'
      adminUsername: 'admin'
      adminPassword: 'admin'
      adminPath: '/wp-admin'
    WPFilesystem:
      wpRootFolder: '%WP_ROOT_FOLDER%'

If set to false, as in the example configuration file above, a number of things will happen:

  1. WordPress code will be loaded in the same scope as the tests
  2. WordPress components (wpdb, wp_query and the like) will be initialized before the tests
  3. request methods like amOnPage, amOnAdminPage, sendAjaxPostRequest all will be resolved in the same scope as the tests
  4. some WordPress functions will be “monkey patched”, thanks to Patchwork, to avoid some shortcomings related to WordPress limits when having to handle more than one request (e.g. the load_template default require_once behaviour).

I’m leveraging the power of the WPTestCase::go_to() method to accomplish all this and “fighting” the background of it to make sure it works for two times in a row.
Due to how WordPress is meant to work (request, spin up WordPress, handle request, exit or die) this is no easy task.

Next

The support for this new functionality will be gradually added and for the time being I’m adding code to support it as my articles about Action-Domain-Responder in WordPress progress and requires it.
The ADR code proved to be, in its unusual nature (in WordPress terms) a perfect proving ground for it.