crosspeak / wp-pest
Pest plugin and tooling for testing WordPress sites and plugins.
Requires
- php: ^8.4
- pestphp/pest: ^5.0
Requires (Dev)
- laravel/pint: ^1.29
- pestphp/pest-plugin-browser: dev-configurable-http-server
- phpstan/phpstan: ^2.1
Suggests
- pestphp/pest-plugin-browser: Required when running browser tests.
Provides
None
Conflicts
None
Replaces
None
README
Pest tooling for testing WordPress sites and plugins.
Modes
The package uses two independent settings instead of four hard-coded modes:
target:siteorplugintests:code,browser, or both
That supports these common runs:
- Existing site browser tests:
target=site,tests=['browser'], withsite.url. - Existing site code tests:
target=site,tests=['code'], with auto-detected or configuredsite.path. - Plugin code tests:
target=plugin,tests=['code'], withplugin.path. - Plugin browser tests:
target=plugin,tests=['browser'], with Docker sandboxes generated from the matrix.
Install
composer require --dev crosspeak/wp-pest
composer require --dev pestphp/pest-plugin-browser # for browser tests
vendor/bin/wp-pest init
By default, init creates a site-mode config for a full WordPress site. You can also be explicit:
vendor/bin/wp-pest init --target=site
Use plugin mode when installing into a plugin repository:
vendor/bin/wp-pest init --target=plugin
init creates example tests for the selected target:
tests/Code/SiteTest.phpandtests/Browser/SiteTest.phpfor site mode.tests/Code/PluginTest.phpandtests/Browser/PluginTest.phpfor plugin mode.
Run the generated code and browser tests with:
vendor/bin/pest
vendor/bin/pest --testsuite=Code
vendor/bin/pest --testsuite=Browser
For plugin browser tests, vendor/bin/pest --testsuite=Browser automatically starts and stops the first generated matrix sandbox. Test runs remove the selected sandbox's WordPress and database volumes before setup so every run starts from a clean installation. The exact WordPress core and dependency plugin packages remain cached in the matrix fixture and are copied into the fresh runtime volume, avoiding repeat downloads after the first run. Use vendor/bin/wp-pest run --tests=browser --matrix=<name> to select a particular combination, or --matrix=all to run every combination. Manual sandbox:start remains non-destructive; use sandbox:reset when you explicitly want to recreate a manually managed sandbox.
Plugin browser tests use an automatic sandbox driver by default. A normal current-PHP/SQLite-compatible run uses the local sandbox; explicit MariaDB requirements, multiple PHP versions, or an explicit Docker selection use Docker. You can override the selection:
WP_PEST_SANDBOX=local vendor/bin/wp-pest run --tests=browser --matrix=<name>
WP_PEST_SANDBOX=docker vendor/bin/wp-pest run --tests=browser --matrix=<name>
The local driver copies the cached WordPress fixture, uses SQLite, runs PHP's built-in server, and executes WP-CLI locally. It requires PHP CLI, the SQLite extension, WP-CLI, and the browser test dependencies. Set sandbox.driver to local, docker, or auto in wp-pest.php; WP_PEST_SANDBOX takes precedence over that setting.
The generated tests/Pest.php bootstraps WordPress before Code test files are discovered, while Browser-only runs skip the local WordPress fixture.
It also increases Pest Browser's default wait timeout from five seconds to one minute.
The generated browser tests assert that the page source includes a document body and that there are no JavaScript errors.
Browser tests for plugin targets can run WP-CLI inside the active matrix sandbox and capture its standard output. Pass each argument separately so commands and PHP snippets are escaped safely:
$formId = wp_pest()->wpCli([
'eval',
'$form_id = GFAPI::add_form(["title" => "Test Form"]); echo $form_id;',
]);
This is useful for seeding browser fixtures and inspecting raw WordPress or plugin database state after UI interactions.
For browser tests, wp-pest also provides namespaced helpers for the common sandbox operations:
use function CrossPeak\WpPest\{actingAsAdmin, wpEval};
$form = wpEval(<<<'PHP'
$form_id = GFAPI::add_form(['title' => 'Test Form']);
$page_id = wp_insert_post([
'post_title' => 'Test Form',
'post_content' => '[gravityform id="'.$form_id.'" title="false"]',
'post_status' => 'publish',
'post_type' => 'page',
]);
return ['form_id' => $form_id, 'path' => wp_make_link_relative(get_permalink($page_id))];
PHP);
visit($form['path'])->assertSee('Test Form');
actingAsAdmin()->navigate('/wp-admin/plugins.php')->assertSee('Plugins');
wpEval() runs the provided PHP body in the active plugin sandbox with $wpdb available and returns its JSON-compatible result. The WordPress site runs in a separate Docker/PHP process, unlike a Laravel app booted inside Pest's process, so WordPress objects and services cannot be used directly in the test process. wpEval() is the RPC boundary for setup and inspection; keep returned values JSON-compatible and use wp_pest()->wpCli() when full WP-CLI output or a non-PHP CLI command is needed. actingAsAdmin() is limited to plugin browser sandboxes, logs in with the seeded admin / password account, and returns a relative-navigation-aware browser page for chaining. These helpers require the optional Pest Browser plugin when used.
For plugin Code tests, the first run creates a real WordPress fixture under .wp-pest/fixtures, backed by the WordPress SQLite integration so no external database service is required. The fixture installs Twenty Twenty-Five from WordPress.org as its default theme, WordPress is installed, and the plugin under test is loaded before the Code suite runs.
Configuration
Create wp-pest.php in the project root:
<?php
return [
'target' => 'plugin',
'tests' => ['code', 'browser'],
'plugins' => [
'dependencies' => [
'../shared/local-plugin',
],
],
'sandbox' => [
'path' => '.wp-pest/sandboxes',
'driver' => 'auto',
],
'matrix' => [
'wordpress' => ['^6.9', '^7.0'],
'php' => ['8.4', '8.5'],
'plugins' => [
// WordPress.org releases are installed through WPackagist.
'query-monitor' => ['3.19.0', '3.20.0'],
// ZIP variants may point at plugins that are not on WPackagist.
'premium-plugin' => [
[
'version' => '1.2.3',
'url' => 'https://example.test/premium-plugin-1.2.3.zip',
],
[
'version' => '1.3.0',
'url' => 'https://example.test/premium-plugin-1.3.0.zip',
],
],
],
'profiles' => [
'default' => [
'tests' => ['code', 'browser'],
],
'compatibility' => [
'tests' => ['code'],
'plugins' => ['woocommerce'],
'groups' => ['woocommerce'],
],
],
'exclude' => [
[
'wordpress' => '^6.9',
'plugins' => ['premium-plugin' => '1.3.0'],
],
[
'profile' => 'compatibility',
'plugins' => ['query-monitor' => '3.19.0'],
],
],
'browser' => [
'database' => 'mariadb:11',
'port' => 8080,
],
'max_combinations' => 100,
],
];
For plugin targets, plugin.path defaults to the current directory. You may set it when the plugin under test is in another directory.
Plugin dependencies are installed and loaded before the plugin under test in both Code fixtures and browser sandboxes. Use a WordPress.org plugin slug, a local directory path, a ZIP URL, or an explicit array with slug, version, and url keys. Explicit ZIP definitions are recommended when the filename does not contain a reliable plugin slug and version.
Only configure ZIP URLs that you trust: the downloaded plugin code is loaded and executed during the test bootstrap.
Every WordPress value, PHP value, plugin-axis value, and profile is combined as a true Cartesian product. The example starts with 2 × 2 × 2 × 2 × 2 = 32 candidates and then applies the partial-match exclusion rules. max_combinations is checked after exclusions. Global plugins.dependencies are inherited by every profile; set inherit_plugins to false, add profile-only plugins, or remove inherited plugins with exclude_plugins when a compatibility suite needs a different environment.
The PHP process running Pest remains controlled by your CI job. Browser sandboxes may additionally define matrix.php as a list of PHP major.minor versions; each value becomes a matrix axis and selects the matching wordpress:phpX.Y-apache and wordpress:cli-phpX.Y images. If matrix.php is omitted, the current PHP minor is used. Code tests still execute in the PHP process running Pest.
Browser sandboxes use the official WordPress image for the selected PHP minor and then install the exact Composer-resolved WordPress matrix version into its shared volume. This avoids relying on Docker tags for every historical WordPress/PHP pairing.
CLI
vendor/bin/wp-pest doctor
vendor/bin/pest
vendor/bin/pest --testsuite=Code
vendor/bin/pest --testsuite=Browser
vendor/bin/wp-pest matrix:list
vendor/bin/wp-pest run # first generated combination
vendor/bin/wp-pest run --matrix=all
vendor/bin/wp-pest run --matrix=default--wp-7.0--query-monitor-3.20.0--premium-plugin-1.3.0
vendor/bin/wp-pest sandbox:start default--wp-7.0--query-monitor-3.20.0--premium-plugin-1.3.0
vendor/bin/wp-pest sandbox:reset default--wp-7.0--query-monitor-3.20.0--premium-plugin-1.3.0
vendor/bin/wp-pest sandbox:destroy default--wp-7.0--query-monitor-3.20.0--premium-plugin-1.3.0
Runtime helpers
wp_pest()->target();
wp_pest()->hasCodeTests();
wp_pest()->hasBrowserTests();
wp_pest()->url();
wp_pest()->wordpressPath();
wp_pest()->pluginPath();
wp_pest()->matrixName();
wp_pest()->matrix();
wp_pest()->wpCli(['plugin', 'list']);
wp_pest()->phpVersion();
wp_pest()->phpVersionIs('>=8.3');
wp_pest()->wordpressVersion();
wp_pest()->wordpressVersionIs('>=6.9');
wp_pest()->hasPlugin('gravityforms');
wp_pest()->pluginVersion('gravityforms');
wp_pest()->pluginVersionIs('gravityforms', '>=2.10.5');
wp_pest()->bootstrapWordPress(); // manual fallback for custom runners
For Pest-style helpers, import CrossPeak\WpPest\actingAsAdmin and CrossPeak\WpPest\wpEval as shown above.
These Boolean helpers make version- or dependency-specific skips straightforward:
test('integrates with Gravity Forms', function (): void {
if (! wp_pest()->hasPlugin('gravityforms')) {
$this->markTestSkipped('Gravity Forms is not active in this matrix combination.');
}
// Compatibility assertions...
});
CI overrides
WP_PEST_TARGETWP_PEST_TESTSWP_PEST_URLWP_PEST_PLUGIN_PATHWP_PEST_WORDPRESS_VERSIONWP_PEST_MATRIXWP_PEST_BROWSER_HOST(host used to reach a plugin browser sandbox; defaults to127.0.0.1)