QA Thing – test-driven developing it 03

Tricking myself into writing code.

Part of a series

This post is the third one in 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 and the second one might be a better starting point to read depending on the skill level.

##The starting code and a 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 this one.
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.
While the plugin code is PHP 5.2 compatible the tests code won’t.

The plugin under development itself

The plugin I’m developing is called “QA Thing” and its purpose is to allow developers to push configuration scripts that will run in the context of a WordPress AJAX request to a plugin repository; QA people will then be able to apply the configurations each plugin provides through the “QA Thing” plugin.
This is meant as a tool internal to developers working in a team but the various areas it touches make the plugin suitable for this kind of article.

Writing a new acceptance test

In the previous article I have “cheated” strictly sticking to the idea of writing as little code as possible to make the test pass.
To avoid my cheating again I will write more and more strict acceptance tests starting with this:

codecept generate:cest acceptance PluginConfigurationRead

I’m using Codeception Cest format to have a test that more closely resembles a PhpUnit based one.
This format allows me to define methods that will run before and after each test, think of PHPUnit setUp and tearDown methods, to clean up whatever fixture I’ve set up.
Before getting down to the code a word about how the “QA Thing” plugin will work: plugins that want to tap into what UI the plugin offers can define “configurations” in the qa folder found in each plugin root folder.
To define configurations and the meta information that describes them developers will push a qa/qa-config.json file.
This file lists the configurations provided by the plugin in the following format:

{
  "configurations": {
    "configuration-one": {
      "title": "Configuration One",
      "target": "some/target/script.php"
    }
  }
}

The plugin should be able to look for and read those configuration files and show them to the user (a “QA Person”).
Here are the test steps:

  • scaffold a plugin
  • copy in its root folder a prototype qa folder
  • check that the plugin shows that configuration in its options page

All of this from an acceptance test.
Since I will need to move/manage files and do that in a WordPress-aware context I will add two modules to the acceptance.suite.yml configuration file:

class_name: AcceptanceTester
modules:
    enabled:
        - \Helper\Acceptance
        - WPDb
        - WPBrowser
        - WPCLI
        - Filesystem
    config:
        WPDb:
            dsn: 'mysql:host=localhost;dbname=wp'
            user: root
            password: root
            dump: tests/_data/dump.sql
            populate: true
            cleanup: true
            url: 'http://wp.dev'
            tablePrefix: wp_
        WPBrowser:
            url: 'http://wp.dev'
            adminUsername: admin
            adminPassword: admin
            adminPath: /wp-admin
        WPCLI:
            path: '/Users/Luca/Sites/wp'

The WPCLI module allows me to use the wp-cli tool from the tests while the Filesystem module allows me to operate and make assertions on the filesystem.

use Codeception\Exception\ModuleException;

class PluginConfigurationReadCest {
    public function _after( AcceptanceTester $I ) {
        try {
            // if not installed status will be != 0 and a ModuleException will be thrown
            $I->cli( 'plugin is-installed acme' );
            $I->cli( 'plugin delete acme' );
        } catch ( ModuleException $e ) {
            // not installed
        }
    }

    /**
     * I can see a plugin provided configuration on the options page
     */
    public function test_i_can_see_a_plugin_provided_configuration_on_the_options_page( AcceptanceTester $I ) {
        $I->am( 'QA person' );
        $I->wantTo( 'see a plugin provided configuration' );

        $I->cli( 'scaffold plugin acme' );

        $pluginPath = $I->cli( 'plugin path acme --dir' );
        $qaFolder = $pluginPath . '/qa';
        $I->copyDir( codecept_data_dir( 'configurations/one-configuration' ), $qaFolder );
        $I->seeFileFound( $qaFolder . '/qa-config.json' );

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

        $I->canSeeNumberOfElements( '#qa-configuration option', 4 );
    }
}

The test fails as the current code will only show 3 configurations in place of the expected 4.

While this test does not dig that deep into details I won’t be able to spoof it as I did before: time to write some code.

Scanning for configurations

Starting from the end (a strong point of test-driven development) the options page will need to be provided a list of available configurations, that list must be compiled scanning installed plugins for a qa/qa-config.json file.
I register a new service provider to the main plugin file:

/**
 * Plugin Name: QA Thing
 * Plugin URI: http://theAverageDev.com
 * Description: WordPress QA for the world.
 * Version: 1.0
 * Author: theAverageDev
 * Author URI: http://theAverageDev.com
 * License: GPL 2.0
 */

include dirname( __FILE__ ) . '/vendor/autoload_52.php';

$container = new tad_DI52_Container();

$container->register('qa_ServiceProviders_Configurations');
$container->register('qa_ServiceProviders_Options');


The qa_ServiceProviders_Configurations service provider will bind a concrete implementation to the qa_Configurations_ScannerI interface; this interface models any class that provides configurations in some manner:

class qa_ServiceProviders_Configurations extends tad_DI52_ServiceProvider {

    /**
     * Binds and sets up implementations.
     */
    public function register() {
        $this->container->singleton('qa_Configurations_ScannerI', 'qa_Configurations_Scanner');
    }
}

The interface concrete implementations is the qa_Configurations_Scanner class:

class qa_Configurations_Scanner implements qa_Configurations_ScannerI {

    /**
     * Returns an array of the available configurations.
     *
     * @return qa_Configurations_ConfigurationI[]
     */
    public function configurations() {
        $configurationProviders = $this->scanPlugins();

        if ( empty( $configurationProviders ) ) {
            return array();
        }

        $configurations = array_map( array( $this, 'buildConfigurations' ), $configurationProviders );

        return call_user_func_array( 'array_merge', $configurations );
    }

    protected function scanPlugins() {
        if ( ! function_exists( 'get_plugins' ) ) {
            include_once ABSPATH . '/' . WPINC . '/plugin.php';
        }

        $plugins = get_plugins();

        if ( empty( $plugins ) ) {
            return array();
        }

        array_walk( $plugins, array( $this, 'addPaths' ) );

        $plugins = array_filter( $plugins, array( $this, 'hasConfig' ) );

        return $plugins;
    }

    protected function addPaths( array &$data, $path ) {
        $data['path'] = WP_PLUGIN_DIR . '/' . $path;
        $data['root'] = dirname( WP_PLUGIN_DIR . '/' . $path );
    }

    protected function hasConfig( array $data ) {
        return is_readable( $data['root'] . '/qa/qa-config.json' );
    }

    protected function buildConfigurations( array $data ) {
        $info = file_get_contents( $data['root'] . '/qa/qa-config.json' );
        $info = (array) json_decode( $info );
        $configurations = array();
        foreach ( $info['configurations'] as $id => $configuration ) {
            $configurations[] = new qa_Configurations_Configuration( $id, (array) $configuration, $data );
        }

        return $configurations;
    }
}

And finally update the qa_Options_Page class to use the provided data:

class qa_Options_Page implements qa_Options_PageI {
    /**
     * @var string
     */
    protected $slug;
    /**
     * @var qa_Configurations_ScannerI
     */
    protected $scanner;

    public function __construct( $slug, qa_Configurations_ScannerI $scanner ) {
        $this->slug = $slug;
        $this->scanner = $scanner;
    }

    public function render() {
        ?>
        <select name="qa-configuration" id="qa-configuration">
            <?php /** @var qa_Configurations_ConfigurationI $configuration */
            foreach ( $this->scanner->configurations() as $configuration ) : ?>
                <option value="<?php echo $configuration->id() ?>"
                        class="qa-configuration-option"><?php echo $configuration->name() ?></option>
            <?php endforeach; ?>
            <option value="foo" class="qa-configuration-option">Foo</option>
            <option value="baz" class="qa-configuration-option">Baz</option>
            <option value="bar" class="qa-configuration-option">Bar</option>
        </select>
        <?php
    }
}

With this new code in place the new acceptance test too will pass:

Next

Time to write another acceptance test? Or to dig into some lower level of testing? I will move the plugin development forward in the next post. For the time being the code shown in the post can be found on GitHub.