WP Test is a library for quickly and easily setting up and executing WordPress unit and integration tests. It allows you to initialize an automated test suite on any new or existing WordPress theme, plugin or full website project.
- Uses the same PHPUnit based framework that powers the WordPress Core test suite, thanks to a mirror repo of the framework.
- CLI command to create and customize config files inside your project required by the Core testing framework.
- Subclass of
WP_UnitTestCase
with additional convenience methods and custom assertions - Enhanced PHPUnit bootstrap process to automatically activate your project's theme and plugins
- In addition to unit testing your code, there is a separate test group for integration tests with external services (Stripe, Facebook, etc.)
- Uses phpspec for designing and unit testing a dependency-free domain model
- Uses Basic setup above to drive integration tests only, treating WordPress itself as an external dependency in specs
- Includes bare minimum of WP classes into phpspec to facilitate common spec use cases without running WordPress
- Includes stubs for most common WP functions
- Uses PhpSpec - PHP-Mock to mock other WP function on demand in specs
Add the Composer package as a dev dependency to your WordPress project:
composer require cyruscollier/wp-test --dev
Run the initialization console command. You may leave off the full path if your system $PATH
already includes your local Composer bin directory.
./vendor/bin/wp-test init
Follow the prompts in the console to configure your testing environment.
- Choose Unit Testing Architecture
- Project namespace
- Source files path
- Path to unit tests
- Path to integration tests (Advanced TDD only)
- Path to WordPress Core directory, relative to project root
- Path to wp-content directory, relative to project root
- Active theme
Required for the PHPUnit/WordPress Core runtime environment. There are many ways to install a mysql server, depending on your operating system. Here are two recommended methods:
Homebrew (Mac OS only)
brew update
brew install [email protected]
brew link [email protected] --force
brew services start [email protected]
mysql -uroot -e "CREATE DATABASE IF NOT EXISTS wp_tests CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
Docker
docker run -d -p 3306:3306 --name wp_tests -e MYSQL_ALLOW_EMPTY_PASSWORD=yes -e MYSQL_DATABASE=wp_tests mysql:5.7
Docker requires a TCP connection to the mysql container, so in wp-tests-config.php
, change DB_HOST
to 127.0.0.1
.
A VM (vagrant, etc.) or Docker mysql server can also be used, but either the server must be setup to accept requests from any host using the configured user over an open port (like 3306), or all test run commands must be executed directly in that environment instead of in the host machine's terminal.
In your project root, run PHPUnit:
./vendor/bin/phpunit
or run the watcher to re-run tests whenever any of your code changes:
./vendor/bin/phpunit-watcher watch
For integration tests:
./vendor/bin/phpunit --group integration
Full PHPUnit documentation: https://phpunit.readthedocs.io/en/7.5/
If using Advanced TDD Mode, run phpspec:
./vendor/bin/phpspec run
or run the watcher to re-run tests whenever any of your code changes:
./vendor/bin/phpspec-watcher watch
Full phpspec documentation: https://www.phpspec.net/en/stable/manual/introduction.html
PHP Constant | Default Value | Description |
---|---|---|
WP_TESTS_MULTISITE |
"0" |
Set to "1" if testing a multisite project |
WP_TESTS_ACTIVATE_THEME |
"twentytwenty" |
Sets the name of the active theme; updated to your theme name during setup |
WP_TESTS_ACTIVATE_PLUGINS |
"1" |
Set to "0" if plugins inside wp-content should not be automatically activated in test environment |
WP_TESTS_INSTALL_PLUGINS |
"" |
If a plugin triggers warnings or errors when automatically activated in the test environment, this usually means that plugin is missing its initial install process. Add directory names, separated by commas, of any plugins that need to run through its install process to work properly in the test environment. Example: "gravityforms,wp-all-import-pro" |
WP_TESTS_ADDITIONAL_PLUGINS |
"" |
Usually only needed when testing a theme or plugin project in isolation that has one or more plugin dependencies. Full site projects should already have all required plugins in wp-content. Include directory paths relative to the project root, separated by commas, of plugins that need to be included in the test environment. Example: "wp-content/plugins/advanced-custom-fields-pro,wp-content/plugins/js_composer" |
WP_TESTS_CONFIG_FILE_PATH |
"wp-tests-config.php" |
Path to special wp-config file used for test environment |
For each PHPUnit test class, extend the WPTest\Test\TestCase
class,
a subclass of WordPress's WP_UnitTestCase
class, which itself extends PHPUnit's PHPUnit\Framework\TestCase
class.
If you add setup()
or teardown()
method to your test class, you must be sure to call the parent method inside it.
WordPress starts a database transaction and prepares global state during setup()
and rolls back the transaction and resets global and other state back to baseline during teardown()
. This setup and teardown process ensures each test method starts with a clean WordPress environment exactly as expected.
Although you have full access to the entire WordPress core API, the test environment Factories are useful for easily creating posts, terms, users and other entities. Dummy data will be added to any database field that is not supplied. For the Post Factory for example, in one line you can create and return a new post with whatever custom data you need on it:
/* Test */
$event_post = $this->factory()->post->create_and_get([
'post_type' => 'event',
'meta_input' => [
'event_start' => '2021-01-01 18:00:00',
'event_end' => '2021-01-01 21:00:00'
]
]);
Like most unit tests, start a test method with setting up whatever state is needed before executing the method/function under test. For WordPress, this usually means one or more of the following:
- Creating WordPress entities: posts, terms, users, etc.
- Adding meta to newly-created entities or adding terms to posts.
- Setting core options or custom options.
- Populating PHP superglobals manually:
$_GET
,$_POST
, etc. - Calling WordPress functions that set various global state:
set_current_screen()
,wp_set_current_user()
, etc. - Manipulating WordPress globals manually:
$post
,$wp_query
, etc.
Then execute the function/method under test and make PHPUnit assertions about how the database or other state has changed. WP Test provides several additional WordPress-specific assertions on top of the ones supplied by WordPress.
Since the WordPress database and global state remains throughout the duration of a single test method, it can sometime be a helpful technique to test several different variations or stages of the function/method under test within one test method, Otherwise you have to recreate the same initial or resulting state in a separate method, which duplicates work and slows down the test suite. Use with caution though, as you still want to only be testing one discreet behavior of your code base in a single test method.
Making real HTTP requests inside unit tests make the test suite slow and brittle, so it's best to mock the request and response.
Assuming your code is using wp_remote_get()
, wp_remote_post()
or similar wrappers of WP_Http
,
use the pre_http_request
filter to make assertions on expected inputs and return a fake response array containing a body
array. You should also simulate returning a WP_Error
object as the response, so your code can handle it appropriately:
/* Source */
function make_api_request($parameter)
{
$response = wp_remote_post('https://yourapi.com/path/to/resource/', [
'body' => [
'keyword' => $parameter,
'apikey' => 'your api key'
]
]);
if (is_wp_error($response)) {
throw new Exception($response->get_error_message());
}
return json_decode($response['body']);
);
/* Test */
add_filter('pre_http_request', function($pre, $parsed_args, $url) {
$this->assertEquals('https://yourapi.com/path/to/resource/', $url);
$this->assertContains(['method' => 'POST', 'body' => [
'keyword' => 'test keyword',
'apikey' => 'your api key'
]], $parsed_args);
return ['body' => json_encode(['message' => 'success']])];
}, 10, 3);
$this->assertEquals(['message' => 'success'], make_api_request('test keyword'));
/* Test */
add_filter('pre_http_request', function($pre, $parsed_args, $url) {
$this->assertContains(['method' => 'POST', 'body' => [
'keyword' => 'test keyword',
'apikey' => 'invalid api key'
]], $parsed_args);
return new WP_Error('http_request_failed', 'Invalid API Key');
}, 10, 3);
$this->expectException(Exception::class);
$this-expectExceptionMessage('Invalid API Key');
make_api_request('test keyword');
Most unit tests are directed at lower-level code and typically won't deal with higher-level application logic like redirects. However, if you decide to unit test application logic like form submissions and redirects, you need to be able to verify the redirect URL without actually outputting a Location header. Outputting the header will trigger a "Headers Already Sent" warning in the test environment because it is an long-running PHP process that isn't serving a response to a browser. Most calls to wp_redirect()
are followed shortly after with exit
/die
, which also can't happen in the test envionment since it will terminate the process. Since a successful wp_redirect()
will return true
, check the return value of wp_redirect()
before exiting using this one-line conditional:
/* Source */
function form_redirect()
{
$redirect_url = 'https://yoursite.com/form-success-page';
return wp_redirect($redirect_url) && exit;
}
Then in your test, add a filter similar to mocking HTTP requests that returns false instead, thereby avoiding the header and exit:
/* Test */
add_filter('wp_redirect', function($url) {
$this->assertEquals('https://yoursite.com/form-success-page', $url);
return false;
});
$this->assertFalse(form_redirect());
Testing PHP output from echo
, printf()
, etc. is not WordPress-specific, but it comes up a lot more because so much of the API requires output to be echoed rather than simply returning a value. To test these scenarios, use output buffering. You can either capture the output directly using ob_get_clean()
and use WP Test's assertHTMLEquals()
assertion to compare it to an expected HTML snippet, or if the data used to prepare the output is more useful or easier to work it, just return it after the output and assert against it.
/* Source */
function output_something()
{
$data = ['thing 1', 'thing 2'];
printf('<ul><li>%s</li></ul>', implode('</li><li>', $data));
return $data;
}
/* Test */
ob_start();
output_something();
$output = ob_get_clean();
$this->assertHTMLEquals('<ul><li>thing 1</li><li>thing 2</li></ul>', $output);
// or
ob_start();
$data = output_something();
ob_get_clean();
$this->assertEquals(['thing 1', 'thing 2'], $data);
Testing hooks involves two parts. First, verify that the hook has been added with its assigned callback. Second, either fire that hook or execute the function directly, and make assertions based on its return value, state change, etc. Make sure the hook's callback is a named function or method, not an anonymous function, so it can be referenced in assertHasAction()
/assertHasFilter()
. It's helpful for action callbacks to return useful data that can be asserted against, even though that return value isn't used in live execution:
/* Source */
function perform_custom_action()
{
// ...
$data = ['thing 1', 'thing 2'];
// ...
return $data;
}
add_action('init', 'perform_custom_action');
/* Test */
$this->assertHasAction('init', 'perform_custom_action');
$this->assertEquals(['thing 1', 'thing 2'], perform_custom_actio());
v1.4 - Switched to WP PHPUnit instead of full Wordpress Core dependency. Remove Composer repository requirement.
v1.3 - Added ability to use in an isolated theme or plugin project, phpunit.xml fixes, expanded and improved documentation.
v1.2 - Setup improvements and fixes, added wp-test reset
command, internal refactoring, updated readme.
v1.1 - Revised wp-test init
command to allow choice to use phpspec or not for unit tests. Improvements and fixes to config templates. Added phpunit-watcher. Added readme.
v1.0.2 - Fix phpunit.xml template to align with WP Core
v1.0.1 - New repo for WP Core, fix config paths and PHPUnit version to WP compatibility
v1.0 - Initial version