Configuring phpunit.xml & boostrap.php

Overview

The PHPUnit/WP_Unit Test framework will derive it’s behavior from two file the phpunit.xml file and the tests/bootstrap.php file.

Each project will require some slight fine-tuning of these files before we can start writing tests.

<?xml version="1.0"?>
<phpunit
bootstrap="tests/bootstrap.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
>
<testsuites>
<testsuite>
	<directory prefix="test-" suffix=".php">./tests/</directory>
	<exclude>./tests/test-sample.php</exclude>
</testsuite>
</testsuites>
</phpunit>
$_tests_dir = getenv( 'WP_TESTS_DIR' );

if ( ! $_tests_dir ) {
$_tests_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib';
}

if ( ! file_exists( "{$_tests_dir}/includes/functions.php" ) ) {
echo "Could not find {$_tests_dir}/includes/functions.php, have you run bin/install-wp-tests.sh ?" . PHP_EOL; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
exit( 1 );
}

// Give access to tests_add_filter() function.
require_once "{$_tests_dir}/includes/functions.php";

/**
* Manually load the plugin being tested.
*/
function _manually_load_plugin() {
require dirname( dirname( __FILE__ ) ) . '/dummy.php';
}

tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );

// Start up the WP testing environment.
require "{$_tests_dir}/includes/bootstrap.php";

PHPUnit.xml

The wp scaffold command creates a file called phpunit.xml.dist. Copy that to phpunit.xml and apply the above tags to get a results along these lines.

Comprehensive documentation available from PHPUnit—keep in mind, we have to use an old version (v7.5) for compatibility.

Element Attributes* Purpose
<phpunit> bootstrap=”tests/bootstrap.php”
backupGlobals=”false”
colors=”true”
convertErrorsToExceptions=”true”
convertNoticesToExceptions=”true”
convertWarningsToExceptions=”true”
Basic configurations for the tests. Generally, we won’t touch these
    <testsuites>
        <testsuite>
name Groupings of tests, saves time wiht large test batteries.
            <directory> prefix
suffix
Where/How to look for tests
    <filter>
        <whitelist>
            <directory>
suffix=”.php” What files the scan for code coverage
        <exclude>
            <directory>
suffix=”.php” Files to omit from the code coverage scan
    <logging>
        <log>
type=”coverage-html”
target=’path/to/coveragereport’
Config for your code coverage report

Basic phpunit.xml example

<?xml version="1.0"?>
<phpunit
	bootstrap="tests/bootstrap.php"
	backupGlobals="false"
	colors="true"
	convertErrorsToExceptions="true"
	convertNoticesToExceptions="true"
	convertWarningsToExceptions="true"
	>
	<testsuites>
		<testsuite name="dummy-plugin-tests">
			<directory prefix="test-" suffix=".php">./tests/</directory>
		</testsuite>
	</testsuites>
	<filter>
		<whitelist>
			<directory suffix=".php">./</directory>

			<exclude>
				<directory suffix=".php">./templates/</directory>
			</exclude>
		</whitelist>
	</filter>

	<logging>
        <log type="coverage-html" target="./tests/report"/>
    </logging>
</phpunit>

Remove the <exclude>./tests/test-sample.php</exclude>. We want this test to fire, to know when the config is good.

Bootstrap.php structure

Your PHPUnit test will start by loading the WordPress Test framework. This primarily works by:

  1. Defining special constantsI particularly like to define WP_CONTENT_DIR here for easy access an expected behavior for your test
  2. Loading the WordPress Test Framework
  3. Hooking your custom code into WordPress’s load
  4. Loading WordPress

WP-CLI will scaffold this slight differently for plugins or themes.

<?php
/**
* PHPUnit bootstrap file.
*
* @package Dummy
*/

if ( PHP_MAJOR_VERSION >= 8 ) {
echo "The scaffolded tests cannot currently be run on PHP 8.0+. See https://github.com/wp-cli/scaffold-command/issues/285" . PHP_EOL; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
exit( 1 );
}

$_tests_dir = getenv( 'WP_TESTS_DIR' );

if ( ! $_tests_dir ) {
$_tests_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib';
}

if ( ! file_exists( "{$_tests_dir}/includes/functions.php" ) ) {
echo "Could not find {$_tests_dir}/includes/functions.php, have you run bin/install-wp-tests.sh ?" . PHP_EOL; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
exit( 1 );
}

// Give access to tests_add_filter() function.
require_once "{$_tests_dir}/includes/functions.php";

Theme bootstrap structure

function _register_theme() {

$theme_dir     = dirname( __DIR__ );
$current_theme = basename( $theme_dir );
$theme_root    = dirname( $theme_dir );

add_filter( 'theme_root', function () use ( $theme_root ) {
return $theme_root;
} );

register_theme_directory( $theme_root );

add_filter( 'pre_option_template', function () use ( $current_theme ) {
return $current_theme;
} );

add_filter( 'pre_option_stylesheet', function () use ( $current_theme ) {
return $current_theme;
} );
}

tests_add_filter( 'muplugins_loaded', '_register_theme' );

The scaffolded bootstrap.php for themes assumes the work will be for a parent theme (ie template_dir=stylesheet_dir). If working on a child theme this will need to be editedChange the ‘pre_option_template’ to the basename for parent theme dir. .

Bootstrap.php structure

/**
* Manually load the plugin being tested.
*/
function _manually_load_plugin() {
require dirname( dirname( __FILE__ ) ) . '/dummy.php';
}

tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );

// Start up the WP testing environment.
require "{$_tests_dir}/includes/bootstrap.php";

The plugins scaffold is very simple — problematically simple. It merely requires your plugins file as part of the muplugins_loaded action. If your plugin depends on other plugins, modifies the database on activation, or does any special stuff, you’ll need to to add those customizations.Don’t worry we’ve got examples

Example 1: Plugin with 2 dependencies

This is a fairly simple example of a configuration for integration testing. This example:

  • Depends on WooCommerce and WooCommerce Subscriptions
  • Loads and Fires each plugin’s activation method to ensure the appropriate database tables are created.
function _manually_load_plugin() {
require dirname(dirname(dirname(__FILE__))).'/woocommerce/woocommerce.php';
require dirname(dirname(dirname(__FILE__))).'/woocommerce-subscriptions/woocommerce-subscriptions.php';
require dirname( dirname( __FILE__ ) ) . '/wsrtp.php';
do_action('activate_/woocommerce/woocommerce.php');
do_action('activate_/woocommerce-subscriptions/woocommerce-subscriptions.php');
do_action('activate_/wsrtp/wsrtp.php');
}

tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );

Example 2: Child Theme with Dependencies

First we’ll load our pluginsNote that I get to use WP_PLUGIN_DIR, that’s because I defined WP_CONTENT_DIR in earlier in this bootstrap.php that our theme depends on.

We’ll also load our parent theme, and finally our custom child theme

function _manually_load_plugins() {
$plugins=[
	'/advanced-custom-fields-pro/acf.php',
	'/bb-plugin/fl-builder.php',
	'/bb-theme-builder/bb-theme-builder.php',
];

foreach ( $plugins as $plugin ) {
	require_once WP_PLUGIN_DIR. "/$plugin";

	do_action( "activate_$plugin" );
}
do_action('plugins_loaded');

}

tests_add_filter( 'wp_loaded', '_manually_load_plugins' );


/**
* Registers theme
*/
function _register_theme() {
$themes=[
'parent'=>WP_CONTENT_DIR . '/themes/bb-theme',
'child'=>WP_CONTENT_DIR . '/themes/bb-theme-child'
];

$parent_theme = dirname( $themes['parent'] );
$child_theme = dirname( $themes['child'] );

$parent_root = dirname( $parent_theme );
$child_root = dirname( $child_theme );

add_filter( 'theme_root', function(){
return WP_CONTENT_DIR . '/themes';
} );

register_theme_directory( WP_CONTENT_DIR . '/themes' );

add_filter( 'pre_option_template', function() use ( $parent_theme ) {
return 'bb-theme';
});
add_filter( 'pre_option_stylesheet', function() use ( $child_theme ) {
return 'bb-theme-child';
});
}

Example 3: Complex Loading Structure

Some plugins will make this very difficult for us. Consider the example, where we have to delve into Restrict Content Pro’s Activate/Upgrade Process to get our database up to snuff.Fortunately, this level of difficulty in unusual.

Ultimately this requires you to dissect and understand your dependencies in depth. Frustrating—but not a bad thing—this pays off when an plugin update breaks your code’s assumptions about the database structure

function _manually_load_plugins() {
if(!class_exists(('RCP_Requirements_Check'))){
$plugins=[
	'/advanced-custom-fields-pro/acf.php',
	'/restrict-content-pro/restrict-content-pro.php',
	'/sfwd-lms/sfwd_lms.php'
];
foreach ( $plugins as $plugin ) {
	require_once WP_PLUGIN_DIR. $plugin;

	`do_action( "activate_$plugin" );`
}
do_action('plugins_loaded');
}
$levels_table=new \RCP\Database\Tables\Membership_Levels();
$levels_table->maybe_upgrade();
$customers_table=new \RCP\Database\Tables\Customers();
$customers_table->maybe_upgrade();
$memberships_table=new \RCP\Database\Tables\Memberships();
$memberships_table->maybe_upgrade();

rcp_options_install();
}


tests_add_filter( 'plugins_loaded', '_manually_load_plugins', 5 );

General Guidelines for bootstrap

1.

Define WP_CONTENT_DIR in the top matter, along with any other special constants for your test case.

Load AND activate any plugins that are dependencies for your tests. Activating is generally necessary to ensure the DB Tables are added to your test database.

2.

If you’re testing a Child theme, you will want to edit the file register the parent theme and to return it in the pre_option_template filter.

3.

The End Goal for Config

As we work through configuring our booostrap.php file. PHPUnit itself will be the guide as to how we’re progressing with the environment set up and configuration. Using the docker-compose run phpunit followed by phpunit that we used to check our environment setup.

You may need to revisit the configuration as you build out your tests and incorporate more dependencies more the core idea is the same—you want to be green

Good Config

Bad Config

Conclusion

The hard part’s over now. Seriously.

 

Remember, we’re did all this extra work because we’re embracing the ‘integration’ nature of Automated testing with WordPress software. By including our dependencies as part of our tests, we’re breaking unit testing rules in favor of increasing our chances that we discover a breaking change if a dependency updates. It’s not testing ‘according to Hoyle’ — but it makes our lives much easier in the long run.

 

Now we get to dig into the fun stuff–writing tests for our code.

×

Keyboard shortcuts

CTRL+Shift+F Search slideshow
F Fullscreen view
CTRL+Click Zoom in
Esc Topic overview
Right arrow,
Down arrow
Next slide
Left arrow,
Up arrow
Previous slide

Color codes

Tooltip
Hover over text more additional info
Link to an external resource
Link to an internal slide
If buttons aren't working, click in the screen to "focus" your browser
RSS
LinkedIn
Share