cyruscollier/wp-test

A framework for running unit and integration tests on WordPress

1.4 2020-09-26 00:47 UTC

This package is auto-updated.

Last update: 2024-04-26 08:35:07 UTC


README

WP Test

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.

Basic Features

  • 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.)

Advanced TDD mode

Installation via Composer

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.

  1. Choose Unit Testing Architecture
  2. Project namespace
  3. Source files path
  4. Path to unit tests
  5. Path to integration tests (Advanced TDD only)
  6. Path to WordPress Core directory, relative to project root
  7. Path to wp-content directory, relative to project root
  8. Active theme

Installing a local MySQL database

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 mysql@5.7
brew link mysql@5.7 --force
brew services start mysql@5.7
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.

Usage

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

Configuration options in phpunit.xml

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

Writing WordPress PHPUnit Tests

Class Setup

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.

Factories & Managing State

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:

  1. Creating WordPress entities: posts, terms, users, etc.
  2. Adding meta to newly-created entities or adding terms to posts.
  3. Setting core options or custom options.
  4. Populating PHP superglobals manually: $_GET, $_POST, etc.
  5. Calling WordPress functions that set various global state: set_current_screen(), wp_set_current_user(), etc.
  6. 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.

Mocking HTTP Requests

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');

Mocking Redirects

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 Output

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

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());

Changelog

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