QA Thing – test-driven developing it 06

Some integration testing.

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

How is integration testing different from unit testing?

In my previous post I’ve gone through the process required to “isolate” my plugin classes from WordPress code using adapters for the purpose of being able to unit test them.
But what is the difference between unit tests and integration tests?
While definitions and discriminations are abundant I think the easiest way to understand it is by looking at some code that’s focused on the WordPress context, the first step is to scaffold a PhpUnit-like test tapping but into the effort WordPress Core developers did to allow developers to run integration tests against WordPress code base using the extensions that wp-browser provides:

wpcept generate:wpunit integration "qa\Configurations\Scanner"

The command will scaffold a basic WordPress integration test that I’m now able to fill with my test methods.
There is some confusion about the definitions and usage of this kind of tests which is aggravated by WordPress lack of a discrete component architecture and autoloading capabilities; the way “pieces” of WordPress are loaded is by including a file and not relying on class autoloading; furthermore much of its code is not object-oriented enough to make it easy to mock.
The biggest difference between the unit tests I wrote and the tests I’m writing here is that WordPress, almost all of it, has been loaded in the same scope as the tests (hence I can access WordPress functions) and that the tests setUp and tearDown methods will provide me with a fresh and empty WordPress installation on each test.
Before delving into the tests I’ve refactored the main plugin file into a class, the qa_Plugin one to allow me global access to the plugin container:

<?php

class qa_Plugin {
    /**
     * @var tad_DI52_ContainerInterface
     */
    protected static $container;

    /**
     * @return tad_DI52_ContainerInterface
     */
    public static function getContainer() {
        return self::$container;
    }

    public static function init() {
        $container = new tad_DI52_Container();

        $container['root-file'] = __FILE__;
        $container['root-dir'] = dirname(__FILE__);

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

        self::$container = $container;
    }
}

and relying on that the main plugin file changed to this:

<?php
/**
 * 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';

qa_Plugin::init();

The first test case I write is for the qa_Configurations_Scanner class and its first test method will just make sure that I can get a working version of it:

<?php

namespace qa\Configurations;

use qa_Configurations_Scanner as Scanner;
use qa_Plugin as Plugin;

class ScannerTest extends \Codeception\TestCase\WPTestCase {
    /**
     * @var \tad_DI52_ContainerInterface
     */
    protected $container;

    public function setUp() {
        // before
        parent::setUp();

        // your set up methods here
        $this->container = Plugin::getContainer();
    }

    public function tearDown() {
        // your tear down methods here

        // then
        parent::tearDown();
    }

    /**
     * @test
     * it should be instantiatable
     */
    public function it_should_be_instantiatable() {
        $scanner = $this->make_instance();

        $this->assertInstanceOf(Scanner::class, $scanner);
    }

    /**
     * @return Scanner
     */
    private function make_instance() {
        return $this->container->make(Scanner::class);
    }
}

Nothing fancy but comforting to see succeed:

[caption id=“attachment_3293” align=“aligncenter” width=“1153”]Instancing works Instancing works[/caption]

Dealing with real objects

The few lines of tests above show that I’m not using mocks and stubs anymore, what $container->make(Scanner::class) returns is a real instance of qa_Configurations_Scanner using real implementations of its dependencies.
The purpose of this kind of test is to verify that the object works outside of its perfect bubble, the one I’ve set up for it in unit tests feeding it the right dependencies at the right time, and that will integrate with its context.
The object context is, in this case, a WordPress installation.
I’m testing “bigger chunks” of the plugin code as with each object I’m als implicitly testing adapters it might use and its dependencies.

Using the code

Adding some methods to the test case I come across a failure:

[caption id=“attachment_3294” align=“aligncenter” width=“1153”]First integration failure First integration failure[/caption]

The failing test method is this:

/**
 * @test
 * it should not provide the plugin example configurations if disabled via option
 */
public function it_should_not_provide_the_plugin_example_configurations_if_disabled_via_option() {
    update_option($this->container->getVar('options.option'), ['disable-examples' => true]);

    $scanner = $this->make_instance();

    $this->assertEmpty($scanner->configurations());
}

The reason why the test is failing is easy to diagnose: I’m reading the option from the database when the qa_Options_Repository is first built; this happens before the test method runs and read option is empty.
The test case is a good canary about how a developer could use the code and expect it to work: if I update the option on the database then I expect it to have an effect on the scanner.
This is a typical finding of integration testing opposed to unit testing: instead of “playing” with new objects purpose-built on each test run I’m using code that has followed the context (WordPress) flow: does my code hold?
The fix is as quick as reading the option in the qa_Configurations_Scanner::configurations() method:

// file src/qa/Configurations/Scanner.php

/**
 * @test
 * it should not provide the plugin example configurations if disabled via option
 */
public function it_should_not_provide_the_plugin_example_configurations_if_disabled_via_option() {
    update_option($this->container->getVar('options.option'), ['disable-examples' => true]);

    $scanner = $this->make_instance();

    $this->assertEmpty($scanner->configurations());
}

to see the test pass:

[caption id=“attachment_3296” align=“aligncenter” width=“1153”]Passing integration tests Passing integration tests[/caption]

Some work later, and sticking to the idea of manipulating the context around the object in place of its dependendencies, I end up with the followint test code:

<?php

namespace qa\Configurations;

use qa_Configurations_ConfigurationI as Configuration;
use qa_Configurations_Scanner as Scanner;
use qa_Plugin as Plugin;

class ScannerTest extends \Codeception\TestCase\WPTestCase {

    /**
     * @var \tad_DI52_ContainerInterface
     */
    protected $container;

    public function setUp() {
        // before
        parent::setUp();

        // your set up methods here
        $this->container = Plugin::getContainer();
    }

    public function tearDown() {
        // your tear down methods here
        foreach (['one', 'two', 'three'] as $dir) {
            $this->rmdir(trailingslashit(WP_PLUGIN_DIR) . $dir);
        }

        // then
        parent::tearDown();
    }

    function rmdir($path) {
        exec("rm -rf $path");
    }

    protected function copyDir($src, $dest) {
        exec("cp -r $src $dest");
    }

    /**
     * @return Scanner
     */
    private function make_instance() {
        return $this->container->make(Scanner::class);
    }

    /**
     * @test
     * it should be instantiatable
     */
    public function it_should_be_instantiatable() {
        $scanner = $this->make_instance();

        $this->assertInstanceOf(Scanner::class, $scanner);
    }


    /**
     * @test
     * it should return the plugin example configurations if no other plugin provides them
     */
    public function it_should_return_the_pluging_example_configurations_if_no_other_plugin_provides_them() {
        $scanner = $this->make_instance();

        $this->assertCount(3, $scanner->configurations());
    }

    /**
     * @test
     * it should not provide the plugin example configurations if disabled via option
     */
    public function it_should_not_provide_the_plugin_example_configurations_if_disabled_via_option() {
        update_option($this->container->getVar('options.option'), ['disable-examples' => true]);

        $scanner = $this->make_instance();

        $this->assertEmpty($scanner->configurations());
    }

    /**
     * @test
     * it should show additional configurations provided by a second plugin
     */
    public function it_should_show_additional_configurations_provided_by_a_second_plugin() {
        update_option($this->container->getVar('options.option'), ['disable-examples' => true]);
        $this->copyDir(codecept_data_dir('plugins/one-configuration'), trailingslashit(WP_PLUGIN_DIR) . 'one');

        $scanner = $this->make_instance();
        $configurations = $scanner->configurations();

        $this->assertCount(1, $configurations);
        $this->assertEquals('one::one', $configurations[0]->id());
    }

    /**
     * @test
     * it should show additional configurations provided by more plugins
     */
    public function it_should_show_additional_configurations_provided_by_more_plugins() {
        update_option($this->container->getVar('options.option'), ['disable-examples' => true]);
        $this->copyDir(codecept_data_dir('plugins/one-configuration'), trailingslashit(WP_PLUGIN_DIR) . 'one');
        $this->copyDir(codecept_data_dir('plugins/two-configurations'), trailingslashit(WP_PLUGIN_DIR) . 'two');
        $this->copyDir(codecept_data_dir('plugins/three-configurations'), trailingslashit(WP_PLUGIN_DIR) . 'three');

        $scanner = $this->make_instance();
        $configurations = $scanner->configurations();

        $this->assertCount(6, $configurations);
        $expected = ['one::one', 'two::one', 'two::two', 'three::one', 'three::two', 'three::three'];
        $read = array_map(function (Configuration $conf) {
            return $conf->id();
        }, $configurations);
        $this->assertEqualSets($expected, $read);
    }

    /**
     * @test
     * it should provide example configurations if not disabled alongside other plugins
     */
    public function it_should_provide_example_configurations_if_not_disabled_alongside_other_plugins() {
        $this->copyDir(codecept_data_dir('plugins/one-configuration'), trailingslashit(WP_PLUGIN_DIR) . 'one');
        $this->copyDir(codecept_data_dir('plugins/two-configurations'), trailingslashit(WP_PLUGIN_DIR) . 'two');
        $this->copyDir(codecept_data_dir('plugins/three-configurations'), trailingslashit(WP_PLUGIN_DIR) . 'three');

        $scanner = $this->make_instance();
        $configurations = $scanner->configurations();

        $this->assertCount(9, $configurations);
        $expected = [
            'one::one',
            'two::one',
            'two::two',
            'three::one',
            'three::two',
            'three::three',
            'qa-thing::example',
            'qa-thing::failing-example',
            'qa-thing::fatal-example'
        ];
        $read = array_map(function (Configuration $conf) {
            return $conf->id();
        }, $configurations);
        $this->assertEqualSets($expected, $read);
    }
}

I’m copying, for the purpose of each test, plugin folders in the plugins directory differently from what I’ve done in unit tests for the same class where I would mock the adapter to spoof the filesystem.

Should I test more?

I’m sticking to the idea that I’m not going to test code that does not fork (contains conditionals) or scale (contains loop).
I write very dumb classes for a reason. Running integration tests on the qa_Configurations_Scanner class allowed me to touch and tests:

  • the adapters
  • the options handling class
  • the configuration objects generation
  • the scanner itself

I would test the options page too if it embedded more logic but I’ve removed logic from it using Handlebars specifically to avoid having to write more test code.

Show me the code

The code is on GitHub tagged post-06 available for anyone to nose around.

Next

I will move to the other side of the plugin: applying and managing configurations.