QA Thing – test-driven developing it 09

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-08.1 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.

Testing a timeout

Before I delve into code I’d like to make a quick recap: I’m testing the part of the plugin in charge of handling the request to apply a configuration.
A configuration is nothing else but a PHP script running in the context of a WordPress AJAX request; these PHP scripts are not under the control of the user that’s trying to apply them or my control (the “QA Thing” plugin developer) and as such I’m trying to handle failures gracefully.
So far I’ve put in place the tests and the code to handle:

  • success
  • failure; in PHP terms this is still a successful script
  • exceptions

The first two cases are “developer-land”: the developer writing the scripts will decide when and if a script did succeed or fail.
The last one is probably an error on the developer side that is triggering an exception; the user applying the configuration should not be left alone when this happens and so I’m handling the case.
The missing case is the one where a script times out.
The reasons might be many but, again, the user should receive some clear feedback about it.
The problem is: how can I test a timeout?

Out to in

The first place to start to add this micro-feature is a test; specifically a functional one.
I had scaffolded a basic time-out focused functional test and it’s time to put some assertions into it:

<?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);
        $I->seeFileFound($this->pluginDir . '/qa/qa-config.json');

    }

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

    // moret tests here...

    /**
     * @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) {
        $I->loginAsAdmin();
        $I->amOnAdminPage('/admin.php?page=qa-options');

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

        $I->seeResponseCodeIs(200);

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

        $I->sendAjaxGetRequest('/wp-admin/admin-ajax.php', ['action' => 'qa_get_last_status']);

        $I->seeResponseContains('fatal-error');
    }

}

I’ve pasted just the last test code and it’s easy to follow the steps:

  1. login as admin user and access the plugin options page
  2. from the page send an AJAX request to apply a configuration I know will time out:
    // file time-out.php
    <?php
    while (true) {
    }
    
    return 0;
    

    2bis. I’m also setting a time limit: I do not care how the back-end will manage to do that but in the tests I do not want to wait 30 or more seconds for that to finish (the standard PHP maximum execution time)

  3. I’m checking the response code is 200; now this is tricky. One would expect a 500 (internal error) but I’m in WordPress land and the back-end will have to hook into the shutdown function to manage the fatal error caused by the time-out and that will always return a 200 HTTP code. This also puts my test in a position where it cannot know if the configuration was applied or not
  4. So I’m checking that an option has been set in the database reporting the last configuration application status
  5. But, when talking to the back-end via AJAX only, there is no possibility to look up an option…
  6. That’s why I’m finally making a second AJAX request to the back-end to GET the last status
  7. And asserting it matches what I expect

The first run is a comprehensible failure:

Failing timeout functional test
Failing timeout functional test

The things to do

The test, as any test, specifies new or updated requirements.
These new requirements are:

  1. allow to set a time limit for the execution of PHP script in the context of an AJAX request
  2. handle a fatal error generated by the script
  3. add a second AJAX request endpoint to get the last configuration application status

Getting the last configuration application status

In a coward move I’m starting from the last point and create the new “endpoint”.
I’m quoting the word “endpoint” as, in stricter terms, there is only one endpoint here: admin-ajax.php but that’s really a router delegating controllers to handle the request; that’s why the AJAX-related classes of the plugin are all named Handler.
After some renaming I’ve added the qa_Ajax_GetLastStatusHandler class to the project:

class qa_Ajax_GetLastStatusHandler implements qa_Ajax_HandlerI {
    /**
     * @var qa_Adapters_WordPressI|WP
     */
    protected $wp;

    /**
     * qa_Ajax_GetLastStatusHandler constructor.
     * @param qa_Adapters_WordPressI $wp
     */
    public function __construct(qa_Adapters_WordPressI $wp) {
        $this->wp = $wp;
    }

    /**
     * Handles the request to get the last configuration application status.
     *
     * @param bool $send Whether the response object should be sent (`true`) or returned (`false`).
     *
     * @return qa_Ajax_Response An AJAX response object.
     */
    public function handle($send = true) {
        $send = false === $send ? false : true;

        $data = $this->wp->get_option('qa-thing-last-run-status', '');
        $response = new qa_Ajax_Response(array('action' => 'get_last_status', 'data' => $data));

        if ($send) {
            $response->send();
        }
        return $response;
    }
}

I’m reading the option from the database and using the qa_Ajax_Response class to send its value.
The class has to be hooked to handle the request somewhere and that “somewhere” is the qa_ServiceProviders_Ajax class:

class qa_ServiceProviders_Ajax extends tad_DI52_ServiceProvider {
    /**
     * Binds and sets up implementations.
     */
    public function register() {
        $applyConfigurationHandler = $this->container->instance('qa_Ajax_ConfigurationApplyHandler');
        $getLastStatusHandler = $this->container->instance('qa_Ajax_GetLastStatusHandler');

        add_action('wp_ajax_qa_apply_configuration', $this->container->callback($applyConfigurationHandler, 'handle'));
        add_action('wp_ajax_qa_get_last_status', $this->container->callback($getLastStatusHandler, 'handle'));
    }
}

I’m again leveraging DI52 instance and callback methods to be able to hook “promises” to the two actions; if I had to write the PHP 5.3+ code needed to do the same I’d have to write:

class qa_ServiceProviders_Ajax extends tad_DI52_ServiceProvider {
    /**
     * Binds and sets up implementations.
     */
    public function register() {
        $this->container->bind('apply-configuration-ajax-handler') = function($container) {
            return $container->make(qa_Ajax_ConfigurationApplyHandler::class);
        };
        $this->container->bind('get-last-status-ajax-handler') = function($container) {
            return $container->make(qa_Ajax_GetLastStatusHandler::class);
        };

        $container = $this->container;

        add_action('wp_ajax_qa_apply_configuration', function() use($container){
            $i = $container->make('apply-configuration-ajax-handler');

            return $i->handle(func_get_args());
        });
        add_action('wp_ajax_qa_get_last_status', function() use($container){
            $i = $container->make('get-last-status-ajax-handler');

            return $i->handle(func_get_args());
        });
    }
}

Handling a fatal error

Fulfilling the other two requests proved a challenge the result of which I’m pasting here; the code comes from the qa_Ajax_ConfigurationApplyHandler class:

<?php

class qa_Ajax_ConfigurationApplyHandler implements qa_Ajax_HandlerI {
    /**
     * @var qa_Configurations_ScannerI
     */
    protected $scanner;

    /**
     * @var bool
     */
    protected $_applied = false;

    /**
     * @var qa_Adapters_WordPressI
     */
    private $wp;

    /**
     * qa_Ajax_AjaxHandler constructor.
     * @param qa_Configurations_ScannerI $scanner
     * @param qa_Adapters_WordPressI $wp
     */
    public function __construct(qa_Configurations_ScannerI $scanner, qa_Adapters_WordPressI $wp) {
        $this->scanner = $scanner;
        $this->wp = $wp;
    }

    /**
     * Handles the request to apply a configuration.
     *
     * @param bool $send Whether the response object should be sent (`true`) or returned (`false`).
     *
     * @return qa_Ajax_Response An AJAX response object.
     */
    public function handle($send = true) {
        $send = false === $send ? false : true;

        if (!isset($_POST['id'])) {
            // ...
        }

        if (!empty($_POST['time-limit'])) {
            if (!filter_var($_POST['time-limit'], FILTER_VALIDATE_INT) || intval($_POST['time-limit']) < 1) {
                // ...
            }

            // `max_execution_time` could be set to '0' but we still have to come up with a number
            $maxTime = max(ini_get('max_execution_time'), 30);
            $timeLimit = intval($_POST['time-limit']);

            // how long did it take to get here? We do not want to cut our legs
            $timeSoFar = $this->timeSoFar();

            // let's give the script a 2 seconds margin
            $newMaxTime = min($maxTime, (float)$timeLimit + $timeSoFar + 2);

            // set the new `max_execution_time`
            ini_set('max_execution_time', ceil($newMaxTime));
        }

        $sanitized = is_string($_POST['id']) ? filter_var($_POST['id'], FILTER_SANITIZE_STRING) : false;
        if (!($sanitized && preg_match('/^.+?::+?/', $sanitized))) {
            // ...
        }

        if (!$this->wp->current_user_can('manage_options')) {
            // ...
        }

        $id = $_POST['id'];
        $configuration = $this->scanner->getConfigurationById($id);

        if (false === $configuration) {
            // ...
        }

        try {
            // register to handle the shutdown and write to the database
            $this->wp->add_action('shutdown', array($this, 'shutdown'));

            $status = $configuration->apply();

            $this->_applied = true;
        } catch (Exception $e) {
            // not really but we are handling it
            $this->_applied = true;
            $data = $this->statusToMessage(-1);
            $response = new qa_Ajax_InternalErrorResponse(array(
                'action' => 'apply_configuration',
                'data' => end($data)
            ));

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

            if ($send) {
                $response->send();
            }
            return $response;
        }

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

        $data = $this->statusToMessage($status);
        $response = new qa_Ajax_Response(array('action' => 'apply_configuration', 'data' => end($data)));

        if ($send) {
            $response->send();
        }

        return $response;
    }

    /**
     * @param int $status
     *
     * @return array
     */
    protected function statusToMessage($status) {
        // ...
    }

    /**
     * @param int $status
     *
     * @return string
     */
    protected function statusToRunStatus($status) {
        // ...
    }

    /**
     * The time it took to get here in seconds.
     *
     * @return int The
     */
    protected function timeSoFar() {
        return isset($_SERVER['REQUEST_TIME_FLOAT']) ?
            (microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']) :
            5; // let's assume it took 5 seconds to get here
    }

    /**
     * Handles the case where the script generated a fatal error.
     */
    public function shutdown() {
        // let's record that we failed miserably
        if (!$this->_applied) {
            $this->wp->update_option('qa-thing-last-run-status', $this->statusToRunStatus(-100));
        } 
    }
}

It’s time to run the test again and see if this made it:

Functional tests passing
Functional tests passing

The code

It’s on GitHub tagged post-09.

Next

I will add the unit and integration tests needed for the new or updated classes and then move to some JavaScript powered acceptance testing.

I appreciate your input