Easier plugin loading in WP Browser

A solution to the plugin loading problem.

The problem

When setting up tests with wp-browser, an extension to Codeception functions to make test driven development of WordPress plugins and themes a practical possibility, I often have been asked the same question: how do I load the plugin I’m testing?
The crucial part to the problem is that the WordPress instance installed for each test case by the wp-browser WPLoader module has no active_plugins option to read; this means no plugin will be loaded.
WP Loader is just a wrapper around wordpress unit tests and so they share the same behaviour.

A first not working approach

Codeception allows for the definition of suite specific bootstrap files, usually the wp-browser WPLoader module is part of the functional test suite, and so a first attempt might be to use the tests_add_filter function to load the plugin main file in the tests/functional/_bootstrap.php file

<?php
// Here you can initialize variables that will be available to your tests

// After mu-plugins have been loaded load the plugin to test
tests_add_filter( 'muplugins_loaded', function () {
    require_once dirname( __FILE__ ) . '/../../my-plugin.php';
} );

The reason this approach will not work is the sequence of Codeception initialization:

  1. Codeception loads the configuration
  2. Codeception initializes and runs each module in the suite
  3. When loaded and run WP Loader will bootstrap WordPress
  4. Tests will kick in at the time of the wp_loaded hook
  5. Codeception reads the suite _bootstrap.php file
  6. The function above will hook the main plugin file require to an hook, mu_plugins_loaded that ran already and will not be called again.

Removing the filter hooking and requiring the pluging main file directly will be half a solution

<?php
// Here you can initialize variables that will be available to your tests
require_once dirname( __FILE__ ) . '/../../intercooler_js_image_lazy_loading.php';

The main plugin file will be required before the tests run, if the plugin implements autoloading then classes will be available but anything that should happen inside the plugin at hooks like plugins_loaded or init will not happen.
A test set up makes things easier to understand

<?php
// file tests/functional/_bootstrap.php

// Here you can initialize variables that will be available to your tests
require_once dirname( __FILE__ ) . '/../../intercooler_js_image_lazy_loading.php';

The test case is simple enough

<?php
// file tests/functional/loadTestTimes.php


class loadTimesTest extends \WP_UnitTestCase {

    protected $backupGlobals = false;

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

        // your set up methods here
    }

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

        // then
        parent::tearDown();
    }

    /**
     * @test
     * it should work
     */
    public function require_time_function_should_exist() {
        $this->assertTrue( function_exists( 'at_require_time' ) );
    }

    /**
     * @test
     * it should work
     */
    public function plugins_loaded_time_function_should_exist() {
        $this->assertTrue( function_exists( 'at_plugins_loaded_time' ) );
    }
}

as is the main plugin file

<?php
// file my-plugin.php

/**
 * Plugin Name: My plugin
 * Plugin URI:  http://theaveragedev.local
 * Description: My plugin
 * Version:     0.1.0
 * Author:      Luca Tumedei
 * Author URI:  http://theaveragedev.local
 * License:     GPLv2+
 * Text Domain: myplugin
 * Domain Path: /languages
 */


function at_require_time(){}

/**
 * Load the plugin
 */
if ( ! function_exists( 'my_plugin_load' ) ) {
    function my_plugin_load() {

        function at_plugins_loaded_time(){}

    }

    add_action( 'plugins_loaded', 'my_plugin_load' );
}

One test will pass and one will fail Plugin failing tests

Manually loading

In the context of the plugin above a solution might be to modify the functional/_bootstrap.php file to not only require the file but also call the my_plugin_load function

<?php
// file tests/functional/_bootstrap.php

// Here you can initialize variables that will be available to your tests
require_once dirname( __FILE__ ) . '/../../intercooler_js_image_lazy_loading.php';
my_plugin_load();

this way both tests will pass Plugin passing tests While this seems to solve the problem manually calling all the functions that missing WordPress filters and hooks are not activating is a pityful and error-prone task. Moving the code above in the test setUp method would be the same solution moved around.

Point 2.5

The ideal solution to tap into tests and WordPress powers is to load the plugins under test at the muplugins_loaded hook.
This will happen after all must-use plugins have been loaded and WordPress is willing and ready to include the active plugins main files.
WordPress unit tests code base supplies the tests_add_filter function to allow for hooking into filters before WordPress is bootstrapped but that’s encapsulated in the code WPLoader uses; implementing this solution requires adding a piece to the Codeception flow schema from before

  1. Codeception loads the configuration 1b. Codeception loads the main bootstrap file (tests/_bootstrap.php)
  2. Codeception inits and runs each module in the suite
  3. When loaded and run WP Loader will bootstrap WordPress
  4. Tests will kick in at the time of the wp_loaded hook
  5. Codeception read the suite _bootstrap.php file

point 1b above opens the possibility to do this in the main bootstrap file

<?php
// file tests/_bootstrap.php

// This is global bootstrap for autoloading
require dirname(dirname(__FILE__)).'/vendor/lucatume/wp-browser/src/includes/functions.php';
tests_add_filter('muplugins_loaded', function(){
    require dirname(dirname(__FILE__)).'/my-plugin.php';
});

and this works as expected.
But this solution is unstable to say the least:

  • I’m loading the functions.php file in a way that’s not flexible at all and very prone to change based on the local set up
  • The plugin is being required in the main bootstrap file and this means that the plugin file will be loaded in unit and acceptance tests as well

The second point means both units and acceptance suites will not have a WordPress instance running in the same scope as the tests so anytime the plugin calls a WordPress method the call will throw a fatal undefined error.

The systematic solution the module proposes

I’ve taken the time to solve the problem in the best possible way I could think of and added an option to the WP Loader module configuration, the default one is extended using the plugins setting

WPLoader:
    wpRootFolder: /Users/Luca/Sites/wp
    dbName: wordpress-tests
    dbHost: 127.0.0.1
    dbUser: root
    dbPassword: root
    wpDebug: true
    dbCharset: utf8
    dbCollate: ''
    tablePrefix: wp_
    domain: wp.dev
    adminEmail: admin@wp.dev
    title: 'WP Tests'
    phpBinary: php
    language: ''
    plugins:
        - my-plugin/my-plugin.php

This allows me to set which plugins should be loaded in which order. This is useful for plugins that are part of a family and might have reduced or no functionalities if a required plugin is missing

WPLoader:
    wpRootFolder: /Users/Luca/Sites/wp
    dbName: wordpress-tests
    dbHost: 127.0.0.1
    dbUser: root
    dbPassword: root
    wpDebug: true
    dbCharset: utf8
    dbCollate: ''
    tablePrefix: wp_
    domain: wp.dev
    adminEmail: admin@wp.dev
    title: 'WP Tests'
    phpBinary: php
    language: ''
    plugins:
        - my-required-plugin-a/plugin-a.php
        - my-required-plugin-b/plugin-b.php
        - my-plugin/my-plugin.php

This will leave the bootstrap files untouched and will allow for an easier Codeception configuration sharing between different developers on the same project.
I’ve added the possibility to call bootstrapping functions through actions after the plugins have been loaded using the bootstrapActions setting

WPLoader:
    wpRootFolder: /Users/Luca/Sites/wp
    dbName: wordpress-tests
    dbHost: 127.0.0.1
    dbUser: root
    dbPassword: root
    wpDebug: true
    dbCharset: utf8
    dbCollate: ''
    tablePrefix: wp_
    domain: wp.dev
    adminEmail: admin@wp.dev
    title: 'WP Tests'
    phpBinary: php
    language: ''
    plugins:
        - my-required-plugin-a/plugin-a.php
        - my-required-plugin-b/plugin-b.php
        - my-plugin/my-plugin.php
    bootstrapActions:
        - activate_my-required-plugin-a/plugin-a.php
        - activate_my-plugin/my-plugin.php

This might prove useful for some plugins relying on activation or deactivation actions to perform some functions that need testing.

On GitHub

The listed new features are available in version 1.6.17 of the module. See the README for information.