This post shares how I setup PHPUnit tests for a WordPress plugin using Composer instead of the WP CLI (the setup command didn’t sound promising for a Windows user) and setting up a whole WP develop site.
This method worked well on Windows and is quite easy if you already have Composer installed. I also copied and tweaked some PHPUnit setup code from Event Espresso originally written by Darren Ethier and Brent Christensen.
How Others Have Setup PHPUNit Tests for WOrdPRess Plugins
Here’s WordPress’ guide to setting up to run PHPUnit Tests for WordPress core, which isn’t quite what I wanted. I wanted PHPUnit to work for my own tests, not WordPress core.
There’s a handy wp cli
command for setting up PHPUnit tests for plugins, but that doesn’t work on Windows (unless you’re using the linux subsystem). That command has the added benefit of setting up tests with Travis CI, but requires you to setup WP CLI and PHPUnit. My instructions let Composer install PHPUnit… So if you already have Composer installed, this might be simpler. Overall though, my instructions are mostly an alternative to the WP CLI option.
How I Did It
Setup Composer Dependency
I had already added a composer configuration for my plugin (actually, Viktor Szepe did and I twaked it) so it was easy adding another dependency to the WordPress Development Repo. Here’s some of the composer.json
file:
"require-dev": {
"phpunit/phpunit": "7.5.9",
"wordpress/wordpress": "dev-master"
},
"repositories": [
{
"type": "git",
"url": "https://github.com/WordPress/wordpress-develop/"
}
],
"scripts": {
"phpunit": "phpunit --configuration tests/phpunit/phpunit.xml"
},
Then I can run composer install
and Composer will download the WordPress development copy (which has all the tests) from GitHub into vendor/wordpress/wordpress
. I can also then run composer run-script phpunit
to run the tests, although they won’t work yet (especially because we haven’t made any yet!)
File Structure
I expect I may someday have Javascript and CodeCeption tests, so I put all the PHPUnit files into a tests/phpunit
folder. That’s where I pu the PHPUnit configuration file too, because the top-level folder of the plugin sometimes gets super congested with all these different configuration files, in my opinion.
In there I put the PHPUnit configuration file, titled phpunit.xml
. It looked like this
<phpunit
bootstrap="bootstrap.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
>
<filter>
<whitelist>
<directory>../../src/PrintMyBlogCentral</directory>
</whitelist>
</filter>
<testsuites>
<testsuite name="default">
<directory suffix=".php">./testcases/</directory>
</testsuite>
</testsuites>
<php>
<ini name="display_errors" value="1" />
</php>
</phpunit>
The filter
element says where to find the plugin’s sourcecode (used for code coverage calculations) so yours will differ. Also the testsuites
element says where the test files will be found, in a subfolder named testcases
. We’ll add those in a minute…
Next to that file PHPUnit configuration file, I added a bootstrap.php
file, which will get started setting up the PHP environment properly.
<?php
/**
* Bootstrap for PMB Unit Tests
*/
require __DIR__ . '/includes/PmbLoader.php';
$core_loader = new PmbLoader();
$core_loader->init();
/**
* Redefining wp_mail function here as a mock for our tests. Has to be done early
* to override the existing wp_mail. Tests can use the given filter to adjust the responses as necessary.
*/
function wp_mail($to, $subject, $message, $headers = '', $attachments = array())
{
return apply_filters('FHEE__wp_mail', true, $to, $subject, $message, $headers, $attachments);
}
So it’s primary job is to load another file, PmbLoader.php
, which has most of the interesting stuff. It will load WordPress’ needed files, and my plugin’s needed files, so PHPUnit can then proceed with testing my code. Here’s what it does:
<?php
class PmbLoader {
public function init()
{
$this->setConstants();
$this->preLoadWPandPmb();
$this->loadWP();
$this->postLoadWPandEE();
$this->requireTestCaseParents();
$this->bootstrapMockAddon();
$this->onShutdown();
}
protected function setConstants()
{
if (! defined('PMB_TESTS_DIR')) {
define('PMB_PLUGIN_DIR', dirname(dirname(dirname(__DIR__))) . '/');
define('PMB_TESTS_DIR', PMB_PLUGIN_DIR . 'tests/phpunit');
define('PMB_MOCKS_DIR', PMB_TESTS_DIR . 'mocks/');
define('WP_TESTS_DIR', PMB_PLUGIN_DIR . 'vendor/wordpress/wordpress/tests/phpunit/');
define('WP_TESTS_CONFIG_FILE_PATH',dirname(dirname(dirname(PMB_PLUGIN_DIR))) . '/wp-tests-config.php');
}
}
protected function preLoadWPandPmb()
{
//if WordPress test suite isn't found then we can't do anything.
if (! is_readable(WP_TESTS_DIR . 'includes/functions.php')) {
die("The WordPress PHPUnit test suite could not be found at: " . WP_TESTS_DIR);
}
require_once WP_TESTS_DIR . 'includes/functions.php';
//set filter for bootstrapping EE which needs to happen BEFORE loading WP.
tests_add_filter('muplugins_loaded', array($this, 'setupAndLoadPmb'));
}
protected function loadWP()
{
require WP_TESTS_DIR . 'includes/bootstrap.php';
}
public function setupAndLoadPmb()
{
require_once PMB_PLUGIN_DIR . 'pmb-api.php';
//save wpdb queries in case we want to know what queries ran during a test
if (! defined('SAVEQUERIES')) {
define('SAVEQUERIES', true);
}
}
public function postLoadWPandEE()
{
// ensure date and time formats are set
if (! get_option('date_format')) {
update_option('date_format', 'F j, Y');
}
if (! get_option('time_format')) {
update_option('time_format', 'g:i a');
}
}
protected function requireTestCaseParents()
{
// good place to require any other files needed by tests, like mock files and test case parent files
}
protected function bootstrapMockAddon()
{
// good place to load any add-on files which we might want to also test
}
protected function onShutdown()
{
//nuke all PMB data once the tests are done, so that it doesn't carry over to the next time we run tests
register_shutdown_function(
function () {
}
);
}
}
That mostly defines some constants, loads some WP and plugin files, does any other adjustments we need to the code for tests (like setting a few filters), and sets up to destroy my plugin’s extra database tables after PHPUnit is finished running.
Tests Config
I still need to create a wp-tests-config.php
file next to wp-config.php
in the WordPress top-level directory, that do the last bit of configuration that’s specific to the environment where this is being tested. Primarily, that’s setting up which test database to use. But the standard sample file for this also doesn’t expect WordPress’ files to be in a plugin’s vendors
folder, so we’ll need to hack it a bit to indicate that. Here’s the wp-tests-config.php
file I used:
<?php
/* Point to the WordPress files in my plugin's vendors folder. */
define( 'ABSPATH', dirname( __FILE__ ) . '/wp-content/plugins/pmb-api/vendor/wordpress/wordpress/src/' );
/*
* Path to the theme to test with.
*
* The 'default' theme is symlinked from test/phpunit/data/themedir1/default into
* the themes directory of the WordPress installation defined above.
*/
define( 'WP_DEFAULT_THEME', 'default' );
/*
* Test with multisite enabled.
* Alternatively, use the tests/phpunit/multisite.xml configuration file.
*/
// define( 'WP_TESTS_MULTISITE', true );
/*
* Force known bugs to be run.
* Tests with an associated Trac ticket that is still open are normally skipped.
*/
// define( 'WP_TESTS_FORCE_KNOWN_BUGS', true );
// Test with WordPress debug mode (default).
define( 'WP_DEBUG', true );
// ** MySQL settings ** //
/*
* This configuration file will be used by the copy of WordPress being tested.
* wordpress/wp-config.php will be ignored.
*
* WARNING WARNING WARNING!
* These tests will DROP ALL TABLES in the database with the prefix named below.
* DO NOT use a production database or one that is shared with something else.
*/
define( 'DB_NAME', '' );
define( 'DB_USER', '' );
define( 'DB_PASSWORD', '' );
define( 'DB_HOST', 'localhost' );
define( 'DB_CHARSET', 'utf8' );
define( 'DB_COLLATE', '' );
/**#@+
* Authentication Unique Keys and Salts.
*
* Change these to different unique phrases!
* You can generate these using the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}
*/
define( 'AUTH_KEY', 'put your unique phrase here' );
define( 'SECURE_AUTH_KEY', 'put your unique phrase here' );
define( 'LOGGED_IN_KEY', 'put your unique phrase here' );
define( 'NONCE_KEY', 'put your unique phrase here' );
define( 'AUTH_SALT', 'put your unique phrase here' );
define( 'SECURE_AUTH_SALT', 'put your unique phrase here' );
define( 'LOGGED_IN_SALT', 'put your unique phrase here' );
define( 'NONCE_SALT', 'put your unique phrase here' );
$table_prefix = 'wptests_'; // Only numbers, letters, and underscores please!
define( 'WP_TESTS_DOMAIN', 'example.org' );
define( 'WP_TESTS_EMAIL', '[email protected]' );
define( 'WP_TESTS_TITLE', 'Test Blog' );
define( 'WP_PHP_BINARY', 'php' );
define( 'WPLANG', '' );
At the very beginning I point ABSPATH
to the copy of WordPress develop installed by Composer in my plugin’s vendors/wordpress/wordpress
. Otherwise it’s the same as usual. You can just add the database name, user, and password, and it should be ready to go.
Test Cases
Now I finally create my first unit test file. I created it inside my plugin in tests/phpunit/testcases
, and named it whatever I wanted, but made sure it ended in Test.php
, eg LicenseCheckTest.php
, and it looked like this:
<?php
class LicenseCheckTest extends WP_UnitTestCase{
public function testIt(){
$this->assertTrue(false);
}
}
From there I follow all the PHPUnit documentation on creating tests, fixtures, etc. Also notice that the class inherits from WP_UnitTestCase
, so it can make use of that classes’ extra stuff. Lastly, I’ll probably end up making my own parent class for holding logic shared between all my tests. I’ll make sure my PmbLoader
‘s requireTestCaseParent
requires that file (or autoloads it) and any mock files I end up using on the way.
Releasing
After adding all these Composer dependencies, when its time to release the plugin again it’s important they don’t get released too.
To remove all these files that are only used for testing and development I run composer install --no-dev
, which removes them all. If I forget to do that before releasing I could accidentally include all those files which would kinda be a disaster.
Reflections
I like how my setup didn’t require installing PHPUnit or setting up a separate WP Develop repository, and then hardcoding its location or requiring an environment variable to point to it so the plugin’s bootstrap process would know where it is.
The main downside is testing with different versions of WordPress. At Event Espresso, they test the plugin against several versions of WordPress in Travis CI, which would probably be a bit easier if the WordPress version weren’t declared in the Composer configuration.
Another obvious downside to this approach is you’ll probably have two versions of WordPress (one used by the plugin during normal requests, and the one installed by Composer for unit tests.)
Lastly, if you mistakenly include the vendor/wordpress/wordpress folder, or the PHPUnit ones, when you release the plugin, the plugin zip file will be huge and so users will probably get an error when download it.
So consider this another option of how to setup PHPUnit tests with plugins.
Also notice that the class inherits from WP_UnitTestCase, so it can make use of that classes’ extra stuff.