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:
- Codeception loads the configuration
- Codeception initializes and runs each module in the suite
- When loaded and run WP Loader will bootstrap WordPress
- Tests will kick in at the time of the
wp_loaded
hook - Codeception reads the suite
_bootstrap.php
file - 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
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 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
- Codeception loads the configuration 1b. Codeception loads the main bootstrap file (
tests/_bootstrap.php
) - Codeception inits and runs each module in the suite
- When loaded and run WP Loader will bootstrap WordPress
- Tests will kick in at the time of the
wp_loaded
hook - 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.