crosspeak / wp-pest
Pest plugin and tooling for testing WordPress sites and plugins.
Requires
- php: ^8.2
- pestphp/pest: ^4.0
Requires (Dev)
- laravel/pint: ^1.29
- phpstan/phpstan: ^2.1
Suggests
- pestphp/pest-plugin-browser: Required when running browser tests.
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-09-15 06:04:38 UTC
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.
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 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',
],
'matrix' => [
'wordpress' => ['^6.9', '^7.0'],
'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, plugin-axis value, and profile is combined as a true Cartesian product. The example starts with 2 × 2 × 2 × 2 = 16 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.
PHP is deliberately controlled by the PHP process running Pest, rather than being another configuration axis. Configure PHP versions in the job matrix of GitHub Actions or your CI system; within each PHP job, wp-pest expands every configured WordPress/plugin/profile combination.
Browser sandboxes use the official WordPress image for the current 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()->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
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)