Foundations

Setting Up PHPUnit for Ability Testing

⏱ 15 min

Before you can test a single permission_callback, you need WP_UnitTestCase available, and that means the WordPress core test library has to exist somewhere PHPUnit can load it. There are two ways to get it: an SVN checkout script that’s been the standard approach for over a decade, or a Composer package that installs the same library as a normal dependency, versioned and reproducible the way the rest of your composer.json already is. This lesson sets up the second approach, since it’s the one that plays best with a CI pipeline you don’t want depending on a shell script and a live SVN mirror.

What you'll learn in this lesson
wp scaffold plugin-tests
The real WP-CLI command that generates your starting test file structure.
The wp-phpunit/wp-phpunit Composer package
A real Packagist package, a versioned mirror of WordPress core's own PHPUnit library.
Why yoast/phpunit-polyfills is a required companion
It isn't installed automatically, and tests fail without it.
Wiring bootstrap.php to the Composer-installed library
Pointing your test bootstrap at the package instead of an SVN checkout.
Prerequisites

A local WordPress environment with WP-CLI available, Composer installed, and a real MySQL database you can point tests at (a fresh, disposable one, WP_UnitTestCase creates and tears down real tables). PHP 8.1 or newer assumed for the code in this lesson.

Step 1: Scaffold the starting files

WP-CLI’s scaffold plugin-tests command generates the standard starting structure, you don’t want to hand-write this from scratch:

# File: terminal
wp scaffold plugin-tests my-plugin --ci=github

This produces tests/bootstrap.php, phpunit.xml.dist, tests/test-sample.php, bin/install-wp-tests.sh, and a starter .github/workflows/phpunit.yml. The install-wp-tests.sh script is the classic SVN-based approach, it works, but it means your CI pipeline is checking out the WordPress core test suite from source on every run rather than resolving it as a pinned Composer dependency. Keep the scaffold’s other files, you’re about to replace how the test library itself gets installed.

Step 2: Require the Composer-based test library

# File: terminal
composer require --dev wp-phpunit/wp-phpunit yoast/phpunit-polyfills

wp-phpunit/wp-phpunit is a real, actively maintained Packagist package built directly from WordPress core’s own develop repository, git://develop.git.wordpress.org, so it stays in sync with WordPress core’s own test suite rather than being a reimplementation of it. yoast/phpunit-polyfills is a required companion, not an optional convenience: WordPress core’s own PHPUnit infrastructure has depended on it since WordPress 5.8.2, and unlike a normal transitive dependency it isn’t installed automatically for you, wp-phpunit/wp-phpunit’s own Composer metadata doesn’t declare it as a dependency, so skip this line and your tests fail with confusing autoloading errors that have nothing obviously to do with a missing package.

Pin the version to match your WordPress target

Because the package is a versioned mirror of WordPress core, you can install an exact matching version, composer require --dev wp-phpunit/wp-phpunit:6.9 for example, if your plugin targets a specific WordPress release rather than always the latest. Left unpinned, Composer resolves the newest available tag, which is usually what you want in active development but worth being deliberate about once you’re stabilizing a release.

Step 3: Point bootstrap.php at the Composer-installed library

The scaffolded bootstrap.php looks for the test library via a WP_TESTS_DIR environment variable, the SVN-checkout convention. Update it to prefer the Composer-installed path first, falling back to the old convention only if it’s set:

// File: tests/bootstrap.php
<?php
require_once dirname( __DIR__ ) . '/vendor/autoload.php';

// Composer autoloader must load before WP_PHPUNIT__DIR becomes available.
$_tests_dir = getenv( 'WP_TESTS_DIR' ) ?: getenv( 'WP_PHPUNIT__DIR' );

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

require_once $_tests_dir . '/includes/functions.php';

function _manually_load_plugin() {
	require dirname( __DIR__ ) . '/my-plugin.php';
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );

require $_tests_dir . '/includes/bootstrap.php';

WP_PHPUNIT__DIR is set automatically once the Composer package is installed and its autoloader has run, no manual path-finding needed. Your plugin’s own bootstrap file loads on muplugins_loaded, the same hook point WP-CLI’s own scaffold template uses, so your abilities are registered on wp_abilities_api_init exactly as they would be on a real site by the time any test runs.

Step 4: Provide a tests config file

The test library needs real database credentials to create its test tables against. Copy the sample config WordPress core ships and point it at your test database:

# File: terminal
curl -sSL -o tests/wp-tests-config.php \
  https://github.com/WordPress/wordpress-develop/raw/trunk/wp-tests-config-sample.php

Edit the copied file’s DB_NAME, DB_USER, DB_PASSWORD, and DB_HOST to match your local test database, and tell the library where to find it via an environment variable rather than hardcoding a path:

<!-- File: phpunit.xml.dist -->
<phpunit bootstrap="tests/bootstrap.php">
	<php>
		<env name="WP_PHPUNIT__TESTS_CONFIG" value="tests/wp-tests-config.php" />
	</php>
	<testsuites>
		<testsuite name="my-plugin">
			<directory suffix="Test.php">./tests/Unit/</directory>
			<directory suffix="Test.php">./tests/Integration/</directory>
		</testsuite>
	</testsuites>
</phpunit>

Splitting Unit/ and Integration/ here on purpose, matching the same layout the MCP Adapter’s own test suite uses, unit tests for pure logic, integration tests for anything touching real WordPress state, keeps a fast subset you can run constantly separate from the slower suite that needs the full WordPress bootstrap.

Step 5: Add a Composer script and run it

{
	"scripts": {
		"test": "phpunit"
	}
}
# File: terminal
composer test

Test it: confirm the sample test actually runs against a real WordPress bootstrap

Replace the scaffolded tests/test-sample.php with a one-line sanity check before writing anything real:

// File: tests/Unit/BootstrapSanityTest.php
<?php
class BootstrapSanityTest extends WP_UnitTestCase {
	public function test_wordpress_functions_are_available(): void {
		$this->assertTrue( function_exists( 'wp_register_ability' ) );
	}
}

Run composer test and confirm it passes. If wp_register_ability doesn’t exist, your WordPress version in wp-phpunit/wp-phpunit predates the Abilities API’s inclusion in core, bump the pinned version.

Table prefix and multisite are configurable too

WP_PHPUNIT__TABLE_PREFIX overrides the default wptests_ prefix if you need it, and the same config file supports multisite testing the same way a normal wp-tests-config.php always has. Neither is needed for the tests this course builds, but both exist if your plugin’s own test suite grows past what’s covered here.

Recap

wp scaffold plugin-tests generates the starting file structure, but swap its SVN-based install-wp-tests.sh convention for wp-phpunit/wp-phpunit plus the required yoast/phpunit-polyfills companion, both real Composer packages that keep the WordPress core test library a pinned, reproducible dependency rather than a shell script your CI pipeline has to run separately. Point bootstrap.php at WP_PHPUNIT__DIR, provide a wp-tests-config.php with real database credentials, and composer test gives you a working WP_UnitTestCase base for every lesson that follows.

Resources & further reading

← Why Testing AI Abilities Is Different From Testing Ordinary PHP Unit Testing permission_callback and execute_callback in Isolation →