TDDing the “Gattiny” plugin – 05

Integrating with the WordPress environment.

Part of a series

This is the fifth post in a series of posts chronicling my attempt to use test-driven development techniques to develop the “Gattiny” WordPress plugin.
The plugin will take care of resizing animated GIF images preserving the animations when uploading them through the WordPress Media screen.
The first post is probably a better starting point than this one.

A first integration test and the first refactoring

My next step to move forward the development and testing of the plugin is, as announced in the previous post will be an integration test.
The purpose of the test will be verifying that the plugin, in its functions, honors and respect the functionalities and conventions it is modifying and replacing; e.g. the actions and filters the class the plugin is extending, the WP_Image_Editor_Imagick one, offers should still be available to alter the plugin behaviour.
For my first integration test I want to make sure the image_make_intermediate_size filter still applies correctly to the saved images; this allows modifying the name the images after those have been resized; this would be the filter to hook to make all thumbnails sepia as an example.
The first step is to scaffold the test case for the filters integration tests:

wpcept generate:wpunit integration Filters

Once the tests/integration/FiltersTest.php file is in place I add the first test to it:

<?php

class FiltersTest extends \Codeception\TestCase\WPTestCase {

    /**
     * It should allow filtering the image_make_intermediate_size filter
     *
     * @test
     */
    public function it_should_allow_filtering_the_image_make_intermediate_size_filter() {
        $filtered = [];
        add_filter('image_make_intermediate_size', function ($filename) use (&$filtered) {
            $filtered[] = $filename;

            return $filename;
        });

        $editor = new gattiny_GifEditor(codecept_data_dir('images/kitten-animated.gif'));
        $editor->load();
        $editor->save();

        $this->assertNotEmpty($filtered);
        $this->assertCount(1,$filtered);
        foreach ($filtered as $filename) {
            $this->assertRegExp('/^.*?kitten-animated.*?\.gif/', $filename);
        }
    }
}

The test case extends the WPTestCase class provided by wp-browser in a test case class that will rely on the WPLoader module; the integration suite configuration is the following one:

# Codeception Test Suite Configuration

# Suite for integration tests.
# Load WordPress and test classes that rely on its functions and classes.


class_name: IntegrationTester
modules:
    enabled:
        - \Helper\Integration
        - WPLoader
    config:
        WPLoader:
            wpRootFolder: /home/luca/Sites/wp
            dbName: wpTests
            dbHost: localhost
            dbUser: root
            dbPassword: root
            tablePrefix: int_wp_
            domain: wp.localhost
            adminEmail: admin@wp.localhost
            title: WP Tests
            plugins: [gattiny/gattiny.php]
            activatePlugins: [gattiny/gattiny.php]

Now, as I run the test, I get the first issue:

This is due to the fact that the file defining the gattiny_GifEditor class is included in the function filtering wp_image_editors:

add_filter('wp_image_editors', 'gattiny_filterImageEditors');
function gattiny_filterImageEditors(array $imageEditors) {
    require_once dirname(__FILE__) . '/src/GifEditor.php';

    array_unshift($imageEditors, 'gattiny_GifEditor');

    return $imageEditors;
}

Putting in place a simple autoloading will solve the issue; to do this I modify the plugin main file to support autoloading of classes:

<?php
/*
Plugin Name: Gattiny
Plugin URI: https://wordpress.org/plugins/gattiny/
Description: Resize animated GIF images on upload.
Version: 0.1.0
Author: Luca Tumedei
Author URI: http://theaveragedev.local
Text Domain: gattiny
Domain Path: /languages
*/

spl_autoload_register('gattiny_autoload');
function gattiny_autoload($class) {
    if (0 !== strpos($class, 'gattiny_')) {
        return false;
    }
    $src = dirname(__FILE__) . '/src/';
    $className = str_replace('gattiny_', '', $class);
    $relativeClassPath = str_replace('_', '/', $className);
    $path = $src . $relativeClassPath . '.php';
    if (!file_exists($path)) {
        return false;
    }

    include $path;

    return true;
}

add_action('admin_init', 'gattiny_maybeDeactivate');
function gattiny_maybeDeactivate() {
    $plugin = plugin_basename(__FILE__);
    if (!empty($_GET['activate']) && is_plugin_active($plugin) && current_user_can('activate_plugins')) {
        return;
    }
    if ('0' === get_option('gattiny_supported')) {
        unset($_GET['activate']);
        add_action('admin_notices', 'gattiny_unsupportedNotice');
        deactivate_plugins(plugin_basename(__FILE__));
    }
}

function gattiny_unsupportedNotice() {
    deactivate_plugins(plugin_basename(__FILE__));
    ?>
    <div class="notice notice-error gattiny_Notice gattiny_Notice--unsupported">
        <p><?php _e('Gattiny is not supported by your server!', 'gattiny'); ?></p>
    </div>
    <?php
}

add_filter('wp_image_editors', 'gattiny_filterImageEditors');
function gattiny_filterImageEditors(array $imageEditors) {
    array_unshift($imageEditors, 'gattiny_GifEditor');

    return $imageEditors;
}

While this test covers the image_make_intermediate_size filter this is not the only filter the plugin is using: the main plugin file is, in fact, filtering the wp_image_editors one too; should I test this one too?
In its current state the function in question is simple enough to result in a “no need to” answer for the question above; yet a deeper thought goes against it.

Things change, refactor for extension

No software project gets simpler over time and having an, initially, simple code base covered by tests means there will be no point where a trade-off between the time it takes to refactor and update the code to make it testable and adding new features has to be made.
In its current form any refactoring seems an excessive virtuosity yet this is the more convenient, in terms of size and effort, time to do that.
Before I write another test I will do the following next:

  • refactor the code to use DI52 as a dependency injection container
  • move the “boilerplate” hooking code into a dedicated service provider class
  • refactor the main plugin file to mere administration operations like including the autoload files and bootstrapping the plugin.

On GitHub

The code shown in this post is on GitHub tagged post-05.