QA Thing – test-driven developing it 08

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

Putting some unit tests back in place

Following the work done in the previous post to make the new functional tests pass I went back to the qa_Configurations_Scanner class to make sure the new “query” method getConfigurationById is unit-tested:

/**
 * Gets a configuration by its id.
 *
 * @param string $id
 * @return qa_Configurations_ConfigurationI|false Either the configuration object or `false` on failure.
 */
public function getConfigurationById($id) {
    $configurations = $this->configurations();
    $this->_searchingId = $id;
    $filtered = array_filter($configurations, array($this, 'matchesId'));

    return empty($filtered) ? false : reset($filtered);
}

The time spent writing proper unit-tests for the qa_Configurations_Scanner::configurations method is paying off: I don’t need to write them again and can base the method qa_Configurations_Scanner::getConfigurationById on it.
The method can have then one of two exit stati:

  1. the configuration was found
  2. the configuration was not found

Two new unit tests are what I need to cover those possibilities:

<?php

namespace qa\Configurations;

use org\bovigo\vfs\vfsStream;
use Prophecy\Argument;
use qa_Configurations_Scanner as Scanner;
use function GuzzleHttp\json_encode;

class ScannerTest extends \Codeception\Test\Unit {

    // [more]

    /**
     * @test
     * it should allow getting a configuration by id
     */
    public function it_should_allow_getting_a_configuration_by_id() {
        $fooConfigurations = [
            'configurations' => [
                'bar' => [
                    'title' => 'Bar',
                    'target' => 'qa/scripts/bar.php',
                ],
                'baz' => [
                    'title' => 'Baz',
                    'target' => 'qa/scripts/baz.php',
                ]
            ]
        ];

        $this->options->read()->shouldBeCalled();
        $plugins = [
            'foo/foo.php' => ['Title' => 'foo']
        ];
        $root = vfsStream::setup('plugins', 777, [
            'foo' => [
                'foo.php' => '<?php // Silence is golden ?>',
                'qa' => [
                    'qa-config.json' => json_encode($fooConfigurations)
                ]
            ]
        ]);
        $this->wp->get_plugins()->willReturn($plugins);
        $this->wp->plugin_dir(Argument::type('string'))->will(function (array $args) use ($root) {
            return $root->url() . DIRECTORY_SEPARATOR . $args[0];
        });

        $scanner = $this->make_instance();

        $barConfig = $scanner->getConfigurationById('foo::bar');

        $this->assertNotEmpty($barConfig);
        $this->assertEquals('Bar', $barConfig->name());
        $this->assertEquals('foo::bar', $barConfig->id());

        $bazConfig = $scanner->getConfigurationById('foo::baz');

        $this->assertNotEmpty($bazConfig);
        $this->assertEquals('Baz', $bazConfig->name());
        $this->assertEquals('foo::baz', $bazConfig->id());
    }

    /**
     * @test
     * it should return false if trying to get non existing configuration by id
     */
    public function it_should_return_false_if_trying_to_get_non_existing_configuration_by_id() {
        $fooConfigurations = [
            'configurations' => [
                'bar' => [
                    'title' => 'Bar',
                    'target' => 'qa/scripts/bar.php',
                ],
                'baz' => [
                    'title' => 'Baz',
                    'target' => 'qa/scripts/baz.php',
                ]
            ]
        ];

        $this->options->read()->shouldBeCalled();
        $plugins = [
            'foo/foo.php' => ['Title' => 'foo']
        ];
        $root = vfsStream::setup('plugins', 777, [
            'foo' => [
                'foo.php' => '<?php // Silence is golden ?>',
                'qa' => [
                    'qa-config.json' => json_encode($fooConfigurations)
                ]
            ]
        ]);
        $this->wp->get_plugins()->willReturn($plugins);
        $this->wp->plugin_dir(Argument::type('string'))->will(function (array $args) use ($root) {
            return $root->url() . DIRECTORY_SEPARATOR . $args[0];
        });

        $scanner = $this->make_instance();

        $this->assertFalse($scanner->getConfigurationById('foo::some'));
    }
}

The new tests are, once again, relying on a virtual file system to avoid messing with the real filesystem and the code did not require any modification.
I’ve update the integration tests accordingly: see the code here.

Testing the AJAX call handling

To support versions of WordPress older than 4.4 I’m relying on WordPress admin-ajax.php “API” to handle the AJAX request to apply a configuration.
The API, as it is, is not that testable as it relies on the use of echo, wp_die, die and exit to return the AJAX request handling result to the caller.
There is no problem testing the this approach on the functional level but it proves difficult on the integration level and impossible on the unit level.
I’ve already shown, in the previous post that a “vanilla” use of the admin-ajax.php mechanics allows for functional testing; while functional testing uses a “white-box” approach to the tested code there is no great magic at work in a test like this:

// file tests/functional/PluginConfigurationApplicationCest.php

/**
 * @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']);
}

When it comes to integration testing the use of wp_die to return the AJAX call result allows for tests to be written filtering the handler:

// file tests/integration/qa/Ajax/HandlerTest.php 

/**
 * @test
 * it should return success response if configuration application was successful
 */
public function it_should_return_success_response_if_configuration_application_was_successful() {
    wp_set_current_user($this->factory()->user->create(['role' => 'administrator']));

    // filter and verify the call to `wp_die` 
    add_filter('wp_die_ajax_handler', function ($message, $title, $args) {
        $this->assertEquals(200, $args['status']);
    });

    $this->copyDir(codecept_data_dir('plugins/scripts-one'), trailingslashit(WP_PLUGIN_DIR) . 'one');
    $_POST['id'] = 'scripts-one::success';

    $handler = $this->make_instance();
    $response = $handler->handle();
}

That proves to be quite easy if the qa_Ajax_Handler code will provide a status value in the return arguments with code like this:

wp_die('','', array('status' => 200));

Sadly the combination of echo and die, wrapped in wp_die as it might be, makes code like that untestable at a unit level.

Making the AJAX handling unit-testable

Beyond this specific case code can be defined “testable” when its input (dependencies are input) is easy to control (set and mock) and its output is easy to check.
The qa_Ajax_Handler::handle() method input is a var in the $_POST global array: not ideal but still very easy to mock and control.
The method output is instead a side effect in two parts:

  1. print to STDOUT
  2. close the request handling with die

I have but put in place an “adapter” to act as a middle-man between the plugin code and WordPress code and this allows me to mock it in unit tests; I could then mock the wp_die method and make assertions on it like this:

// file tests/unit/qa/Ajax/HandlerTest.php 

/**
 * @test
 * it should return success response if configuration application was successful
 */
public function it_should_return_success_response_if_configuration_application_was_successful() {
    $configuration = $this->prophesize(Configuration::class);
    // 0 is success
    $configuration->apply()->willReturn(0);

    $scanner = $this->prophesize(Scanner::class);
    $scanner->getConfigurationById('foo::bar')->willReturn($configuration->reveal());

    $wp = $this->prophesize(WP::class);
    $wp->wp_die('','', 200)->shouldBeCalled();

    $_POST['id'] = 'foo::bar';

    $handler = $new Handler($scanner->reveal(), $wp->reveal());

    $response = $handler->handle();
}

But this does not solve issue #1: the echo part.
To be able to test that too I’ve put in place some value objects extending WordPress WP_Ajax_Response class.
The basic one below represents a “good” response with a success status of 200 and is a modification over the basic WP_Ajax_Response class to return more meaningful HTTP response codes in the wp_die function call in the send method:

<?php

class qa_Ajax_Response extends WP_Ajax_Response {
    const ERROR = 0;
    const SUCCESS = 1;

    /**
     * @var int
     */
    protected $status = 200;

    /**
     * @var
     */
    protected $id = self::SUCCESS;

    /**
     * @var mixed|string
     */
    protected $action;

    /**
     * @var mixed|string
     */
    protected $data;

    /**
     * qa_Ajax_Response constructor.
     *
     * @param array|string $args
     */
    public function __construct($args) {
        $this->action = isset($args['action']) ? $args['action'] : 'action';
        $this->data = isset($args['data']) ? $args['data'] : '';

        $args = array(
            'what' => $this->action,
            'action' => 'qa_' . $this->action,
            'id' => $this->id,
            'data' => $this->data
        );

        $this->add($args);
    }

    /**
     * Display XML formatted responses and sends the response header status.
     *
     * Differently from the base WP_Ajax_Response object it will return a different HTTP status.
     *
     * Sets the content type header to text/xml.
     */
    public function send() {
        header('Content-Type: text/xml; charset=' . get_option('blog_charset'));
        echo "<?xml version='1.0' encoding='" . get_option('blog_charset') . "' standalone='yes'?><wp_ajax>";
        foreach ((array)$this->responses as $response) {
            echo $response;
        }
        echo '</wp_ajax>';
        if (wp_doing_ajax()) {
            wp_die(null, null, $this->status);
        } else {
            die();
        }
    }
}

This basic object is then extended in specialized responses like the one dealing with a “bad request”:

<?php

class qa_Ajax_BadRequestResponse extends qa_Ajax_Response {

    /**
     * @var int
     */
    protected $status = 400;

    /**
     * @var int
     */
    protected $id = self::ERROR;
}

I’ve finally modified the qa_Ajax_Handler::handle method to support an optional $send argument that I will use in tests to trigger a send of the response object or its return:

/**
 * Handles the request to handle 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);

With this value objects in place testing the qa_Ajax_Handler class on an integration and unit level becomes finally easier:

[caption id=“attachment_3307” align=“aligncenter” width=“1181”]Update unit tests pasing Update unit tests pasing[/caption]

[caption id=“attachment_3308” align=“aligncenter” width=“1181”]Update integration tests pasing Update integration tests pasing[/caption]

While similar the code of the integration tests and unit tests differs, once again, in what and how much I’m mocking to set up the fixture: in the unit tests I’m testing the class in isolation mocking every input and dependency while in integration tests I manipulate the class environment to make it behave the way I need it to.

The code

As the code progresses I will post it to the plugin repository on GitHub; the code I’m showing in this post is tagged post-08.1 on GitHub.