QA Thing – test-driven developing it 07

Part of a series

This post is part of a series of posts I’m publishing to chronicle my personal test-driven development flow to develop a WordPress plugin.
The first post in the series could be a better starting points than this post.

##The starting code and the tools
I’m pushing the code to GitHub as I work tagging it whenever a relevant change happens.
The code I’m starting from for this post is tagged post-06 on GitHub.
As tools of the trade I’m relying on wp-browser and Codeception for the testing, DI52 as dependency injection container and xrstf PHP 5.2 compatible autoloader for Composer to autoload the classes.

Thinking out the script runner

To refocus on the task at hand it’s worth pointing out what the plugin is supposed to do in some detail.
The plugin is named “Qa Thing” as it was born out of a need of mine to “share” PHP scripts (“configurations” in the plugin jargon) in a team: while bugs and starting setups are very often easy to replicate using WordPress UI that’s not always the case; think about having to set up a context like “I have 3 scheduled posts that have not been published at the scheduled time” without accessing the database.
To get around these limitations the plugin acts as a “bridge” between developers that can write the code that will create the starting setup and the “QA Person” that will be able to apply it via the plugin provided UI.
I’ve covered the UI (from a functional point of view) in the earlier posts and so far the only provided function is scanning the existing plugins for configurations and showing them to the user: it’s now time to use that choice to apply the chosen configuration and see its effect.
The best way to convey the concept is probably the animated image found on the repository main page.

[https://github.com/lucatume/qa-thing/blob/master/doc/images/configuration-apply.gif]

When I say “apply a configuration” what I mean is “running a PHP script” on behalf of the user.
Keeping this in mind, on a functional level, the interaction is quite simple:

  • the user selects a configuration
  • each configuration defines a target script (this script is provided by the plugin developers)
  • the choice is sent to the back-end that will find the script and will run it

To avoid killing the screen if the script generates any kind of error the plugin should execute the scripts in an “isolated” context and that context will be an AJAX call.
As a first iteration the objective of the UI will be to report one of three stati to the user about the script execution results:

  • Done – the script executed successfully without errors, the definition of “successfully” is up to the developer writing the script
  • Failed – the script executed without errors but failed, the definition of “failed” is up to the developer writing the script
  • Error – the script either timed out or generated an error

I will move into the next code keeping this simple objective in mind.

Writing a functional test for it

I’m writing three new functional tests and the first step is to generate a new Cest format test to run them:

codecept generate:cest functional PluginConfigurationApplicaiton

After wp-browser provided me with a basic structure I fill in the draft flow of a successful, failing and exception raising configuration:

<?php

use function tad\WPBrowser\Tests\Support\rrmdir;

class PluginConfigurationApplicationCest {

    /**
     * @var string
     */
    protected $pluginDir;

    public function _before(FunctionalTester $I) {
        $this->pluginDir = $I->getWpRootFolder() . '/wp-content/plugins/acme';
        rrmdir($this->pluginDir);
        $I->copyDir(codecept_data_dir('plugins/scripts-one'), $this->pluginDir);
    }

    public function _after(FunctionalTester $I) {
        rrmdir($this->pluginDir);
    }

    /**
     * @test
     * it should allow applying a successful configuration and see the success status
     */
    public function it_should_allow_applying_a_successful_configuration_and_see_the_success_status(FunctionalTester $I
    ) {
        $I->seeFileFound($this->pluginDir . '/qa/qa-config.json');

        $I->loginAsAdmin();
        $I->amOnAdminPage('/admin.php?page=qa-options');

        $I->sendAjaxPostRequest('/wp-admin/admin-ajax.php',
            ['action' => 'qa_apply_configuration', 'id' => 'scripts-one::success']);

        $I->seeResponseCodeIs(200);

        $I->seeOptionInDatabase(['option_name' => 'qa-thing-last-run-status', 'option_value' => 'success']);
    }

    /**
     * @test
     * it should allow applying a failing configuration and see the failure status
     */
    public function it_should_allow_applying_a_failing_configuration_and_see_the_failure_status(FunctionalTester $I) {
        $I->seeFileFound($this->pluginDir . '/qa/qa-config.json');

        $I->loginAsAdmin();
        $I->amOnAdminPage('/admin.php?page=qa-options');

        $I->sendAjaxPostRequest('/wp-admin/admin-ajax.php',
            ['action' => 'qa_apply_configuration', 'id' => 'scripts-one::failure']);

        $I->seeResponseCodeIs(200);

        $I->seeOptionInDatabase(['option_name' => 'qa-thing-last-run-status', 'option_value' => 'fail']);
    }


    /**
     * @test
     * it should allow applying a configuration generating an error and see the error status
     */
    public function it_should_allow_applying_a_configuration_generating_an_error_and_see_the_error_status(
        FunctionalTester $I
    ) {
        $I->seeFileFound($this->pluginDir . '/qa/qa-config.json');

        $I->loginAsAdmin();
        $I->amOnAdminPage('/admin.php?page=qa-options');

        $I->sendAjaxPostRequest('/wp-admin/admin-ajax.php',
            ['action' => 'qa_apply_configuration', 'id' => 'scripts-one::error']);

        $I->seeResponseCodeIs(200);

        $I->seeOptionInDatabase(['option_name' => 'qa-thing-last-run-status', 'option_value' => 'error']);
    }

    /**
     * @test
     * it should allow applying a configuration timing out and see the error status
     */
    public function it_should_allow_applying_a_configuration_timing_out_and_see_the_error_status(FunctionalTester $I) {
        // this requires more thinking
    }
}

I’m triggering an AJAX action calling WordPress admin-ajax.php script; while going with a REST API based approach would be “cooler” in this case it’s not the fastest way in terms of code and I will be able to update the code later should I want to do that; plus the plugin should support older versions of WordPress and the REST API infrastructure was introduced in 4.4.
The qa_apply_configuration action is paired with a configuration id to uniquely identify the configuration that should be applied; the nonce is missing for the time being and I will add it later.

The bare minimum code to make the tests pass

To handle the new flow I’ve added a new service provider to the plugin:

<?php

class qa_ServiceProviders_Ajax extends tad_DI52_ServiceProvider {
    /**
     * Binds and sets up implementations.
     */
    public function register() {
        $this->container->bind('qa_Ajax_AjaxHandlerI', 'qa_Ajax_AjaxHandler');
        $handler = $this->container->instance('qa_Ajax_AjaxHandlerI');

        add_action('wp_ajax_qa_apply_configuration', $this->container->callback($handler, 'handle'));
    }
}

Using DI52 lazy instantiation (see the instance method) and callback (see the callback method ) to hook the handle method of the qa_Ajax_AjaxHandler class to the request handling flow.
The qa_Ajax_AjaxHandler class itself will fetch the configuration, try to apply it then update and return a result status:

<?php

class qa_Ajax_AjaxHandler implements qa_Ajax_AjaxHandlerI {
    /**
     * @var qa_Configurations_ScannerI
     */
    protected $scanner;

    public function __construct(qa_Configurations_ScannerI $scanner) {

        $this->scanner = $scanner;
    }

    /**
     * Handles the request to handle a configuration.
     *
     * @return array An array of data representing the configuration application status.
     */
    public function handle() {
        $id = $_POST['id'];
        $configuration = $this->scanner->getConfigurationById($id);

        try {
            $status = $configuration->apply();
        } catch (Exception $e) {
            $status = -1;
        }

        update_option('qa-thing-last-run-status', $this->statusToRunStatus($status));

        $data = $this->statusToMessage($status);

        wp_die(json_encode($data));
    }

    /**
     * @param int $status
     *
     * @return string
     */
    protected function statusToMessage($status) {
        $map = array(
            0 => __('Success! The configuration was successfully applied.', 'qa'),
            1 => __('Failure... The configuration was applied correctly but something went wrong.', 'qa'),
            -1 => __('Error! The configuration generated one or more errors during its application.', 'qa'),
            -33 => __('Error! The configuration target script cannot be found.', 'qa'),
            -100 => __('Time out! The configuration target script timed out.', 'qa'),
        );

        $unknown = __('The exit status returned by the configuration is not a recognized one.', 'qa');

        return isset($map[$status]) ?
            array('status' => $status, 'message' => $map[$status])
            : array('status' => $status, 'message' => $unknown);
    }

    /**
     * @param int $status
     *
     * @return string
     */
    protected function statusToRunStatus($status) {
        $map = array(
            0 => 'success',
            1 => 'fail',
            -1 => 'error',
            -33 => 'not-found',
            -100 => 'time-out',
        );

        return isset($map[$status]) ? $map[$status] : 'unknown';
    }
}

While the tests are not consuming the response yet I’m updating an option on the database to record a script last execution status and check it in the tests.

Do I need write some lower level code?

Indeed.
I’ve modified existing classes to add the code I needed (configuration, scanner) and added a new one, the AJAX handler, that forks and contains some non trivial logic.
In the next post I will move from functional testing to the unit and integration level to make sure the new code can handle itself.
I will get back to the functional tests when I’m done to update them to include authentication in the request.

The code

The code for this post can be found on GitHub

I appreciate your input