Adding unit tests to your WordPress plugin using wp-env

man in white shirt using macbook pro

Earlier this week, I spent some time adding unit tests to my WordPress starter plugin. I’ve worked on this plugin for almost a decade, and use it as a place to explore and learn the latest developments in WordPress.

Adding unit tests is important for the integrity of your code as the codebase increases in size and complexity, and as a thought experiment for writing more considered code.

I took this opportunity to create something, as well as use the latest tech to make unit testing more accessible. Here are a few tips for using @wordpress/env to add unit tests to your plugin.

The wp-env project handbook page has links to instructions on how to install each dependency. It’s really the best overall detailed guide for using @wordpress/env. Consider the rest of this post as my experience of the tool and my tips, rather than as the official guide. Bookmark this page. You’ll refer to it often while setting up your environment.

I’d love to hear from you on how I can improve the wp-env and unit tests integration in my starter plugin. Add feedback in the comments below, or submit pull requests on GitHub. Patches welcome!

Install Git, Node, NPM, and Docker

If you don’t already have them available, be sure to install Git, Node, NPM, and Docker Desktop on your machine. This sounds like a lot, though all tools are useful outside of this tutorial as well, in particular Git.

Once you have Git, Node, NPM, and Docker Desktop available, there are a few options available to you for developing your unit test runner. This is a script which runs your tests at various points in development, either by manually calling a specific command in your Terminal, by adding a Git hook or running via continuous integration (CI) tools.

If you have an existing development environment for your plugin, you can use WP-CLI to install a test suite. To do this, you’d run the wp scaffold plugin-tests <your-plugin-slug> command. This requires that you have WP-CLI installed as well.

For this exercise, I chose to use @wordpress/env to run my tests. @wordpress/env (or wp-env) is an NPM package which uses Docker to set up a development environment for your WordPress plugin, with a dedicated test site and integrated with your plugin’s tests.

Being containerized, wp-env introduced a few new learning curves for me as well, slightly different to the common PHPUnit tutorials online. Those sparked this post.

Adding wp-env to your project

The official wp-env guide has some good steps to follow to get started. Here’s how I started:

I chose to install wp-env specific to my project, instead of globally. This ensures anyone doing development on my starter plugin has the same development environment and can get started really easily. Also, all the tooling is containerized from this point forward.

To install wp-env local to your project, run the following command:

npm i @wordpress/env --save-dev

At this point, you’d also likely want to have Composer installed, to package up and install PHP dependencies.

In your package.json file, adding the following is a useful shortcut:

"scripts": {
    "wp-env": "wp-env"
}

One trade-off of the localized installation is that all commands need to be prefixed with npm run. This is a small trade-off, alongside needing to add an extra set of -- to any command which takes in arguments.

To get your environment running, run the following in your Terminal:

cd ~/your-plugin-directory-name
npm run wp-env start

Hey presto. You now have a WordPress test environment which includes your plugin. Inside your plugin, add a tests/ directory with a test-sample.php file containing the following sample test (the scaffold command in WP-CLI will do this for you as well):

<?php
/**
 * Class SampleTest
 *
 * @package Sample_Plugin
 */

/**
 * Sample test case.
 */
class SampleTest extends WP_UnitTestCase {

	/**
	 * A single example test.
	 */
	function test_sample() {
		// Replace this with some actual testing code.
		$this->assertTrue( true );
	}
}

You can now run PHPUnit, and your test will be detected and show that it has passed.

In my starter plugin, I added a shortcut to package.json to make this repeatable process into shorter command. I can now run npm run test:unit which will output wp-env run tests-cli --env-cwd=wp-content/plugins/starter-plugin ./vendor/bin/phpunit.

Note: The use of --env-cwd=wp-content/plugins/starter-plugin is important here, so wp-env knows that we’re running tests in the context of your plugin and not WordPress core. I also call /vendor/bin/phpunit instead of simply phpunit as many people (read: probably only me) have a copy of PHPUnit installed globally on their machine, which causes conflicts with where your wp-env looks for PHPUnit and the corresponding code.

Add a .wp-env.json file to your project

To set up @wordpress/env using a common configuration across machines, one can add a .wp-env.json file to your project. When developing a plugin, this is crucial.

Here’s my file:

{
    "core": "WordPress/WordPress",
    "plugins": [ "." ],
    "env": {
        "tests": {
            "phpVersion": "8.1"
        }
    }
}

In short, this file makes sure we’re using the latest WordPress build in our enviroments, mounts the current directory as a plugin (this is the plugin we’re developing), and sets the PHP version on our test enviroment to version 8.1. No doubt this file will develop over time. The env section is likely able to be removed now, though I wanted to ensure I’m running at least PHP 8.1 for these tests.

Built in test suite

wp-env includes two sites; cli and tests-cli. Any commands can be run on either site. The tests will be run on tests-cli, which includes a full PHPUnit setup with the core WordPress tests.

PHPUnit requires a configuration file, a bootstrap file, and a directory of tests to run. The configuration file shares how the test files are named, which directory they are in, and where to look for the bootstrap file.

Here’s the config file I ended up with:

<?xml version="1.0"?>
<phpunit
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  bootstrap="tests/bootstrap.php"
  backupGlobals="false"
  colors="true"
  xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd">
  <php>
    <env name="WORDPRESS_TABLE_PREFIX" value="wp_"/>
  </php>
  <testsuites>
    <testsuite name="default">
      <directory prefix="test-" suffix=".php">tests/</directory>
    </testsuite>
  </testsuites>
</phpunit>

https://github.com/mattyza/starter-plugin/blob/trunk/phpunit.xml.dist

This file is the tip of the iceberg. It can be configured in a bunch of other ways. For now, we’re keeping it simple.

The bootstrap file loads in all necessary files and packages to be able to run the tests. Here’s the bootstrap I ended up with:

<?php
/**
 * PHPUnit bootstrap file.
 *
 * @package Starter_Plugin
 */

$_tests_dir = getenv( 'WP_TESTS_DIR' );

// Forward custom PHPUnit Polyfills configuration to PHPUnit bootstrap file.
$_phpunit_polyfills_path = getenv( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH' );
if ( false !== $_phpunit_polyfills_path ) {
	define( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH', $_phpunit_polyfills_path );
}

require 'vendor/yoast/phpunit-polyfills/phpunitpolyfills-autoload.php';

// Give access to tests_add_filter() function.
require_once "{$_tests_dir}/includes/functions.php";

/**
 * Manually load the plugin being tested.
 */
function _manually_load_plugin() {
	require dirname( dirname( __FILE__ ) ) . '/starter-plugin.php';
}

tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );

// Start up the WP testing environment.
require "{$_tests_dir}/includes/bootstrap.php";

https://github.com/mattyza/starter-plugin/blob/trunk/tests/bootstrap.php

You’ll notice I’m including the phpunit-polyfills file directly here. When using the Composer autoloader, the tests didn’t run. This is something I’m exploring further to make this file even shorter and more sustainable in the long term.

Writing the actual tests

Now after all that setup, we get to the meat of the project- writing the actual tests.

Tests should test for a single use case, and should test that the function does what it is supposed to do. It is very easy to unknowingly test WordPress functions instead of testing your own code. Be sure to scrutinize your tests to ensure they are testing the output of your own functions.

For our tests, each file in the tests directory should be named starting with test-, and ending with .php. From there, the class inside the file should extend the WP_UnitTestCase class.

There are several methods available to set up and tear down the class itself when loading and unloading. These are useful for setting up common objects used in tests.

As a start, I added unit tests for the main Starter_Plugin class.

<?php
/**
 * Class Test_Starter_Plugin
 *
 * @package Starter_Plugin
 */

/**
 * Sample test case.
 */
class Test_Starter_Plugin extends WP_UnitTestCase {
	public function set_up() {
        parent::set_up();
        
        // Mock that we're in WP Admin context.
		// See https://wordpress.stackexchange.com/questions/207358/unit-testing-in-the-wordpress-backend-is-admin-is-true
        set_current_screen( 'edit-post' );
        
        $this->starter_plugin = new Starter_Plugin();
    }

    public function tear_down() {
        parent::tear_down();
    }

	public function test_has_correct_token() {
		$has_correct_token = ( 'starter-plugin' === $this->starter_plugin->token );
		
		$this->assertTrue( $has_correct_token );
	}

	public function test_has_admin_interface() {
		$has_admin_interface = ( is_a( $this->starter_plugin->admin, 'Starter_Plugin_Admin' ) );
		
		$this->assertTrue( $has_admin_interface );
	}

	public function test_has_settings_interface() {
		$has_settings_interface = ( is_a( $this->starter_plugin->settings, 'Starter_Plugin_Settings' ) );
		
		$this->assertTrue( $has_settings_interface );
	}

	public function test_has_post_types() {
		$has_post_types = ( 0 < count( $this->starter_plugin->post_types ) );
		
		$this->assertTrue( $has_post_types );
	}

	public function test_has_load_plugin_textdomain() {
		$has_load_plugin_textdomain = ( is_int( has_action( 'init', [ $this->starter_plugin, 'load_plugin_textdomain' ] ) ) );
		
		$this->assertTrue( $has_load_plugin_textdomain );
	}
}

https://github.com/mattyza/starter-plugin/blob/trunk/tests/test-starter-plugin.php

This file is quite short, as the class it’s testing is a small class. In each case, I’m testing with assertTrue to check that all the desired values are correct. I’m using the set_up method to instantiate a single instance of Starter_Plugin instead of doing that inside each test. It is also important to call parent::set_up() and parent::tear_down() appropriately so the extended class can do it’s work. You’ll also notice the use of set_current_screen here as a way of mocking that we’re inside WP Admin (certain code in this class runs only in the WP Admin context).

Quick notes

That’s really all there is to it. There are plenty of options for adding this test runner to your development workflow, from integrating with Travis CI (again, a file is generated for you when running wp scaffold plugin-tests), running via GitHub Actions, or via other continuous integration tools. It is also useful to generate a coverage report, send this to CodeCov or Coveralls, and monitor your test coverage over time. These are topics for a different post.

A quick rundown:

  • Get the tools you need to run @wordpress/env. Git, Node, NPM, and Docker.
  • Have Composer installed for added benefits. You can run Composer already via the environments wp-env builds for you.
  • Run commands within the context of the appropriate environment, whether cli or tests-cli.
  • Use package.json to make shortcuts for long commands you want to run often.
  • Call PHPUnit directly via ./vendor/bin/phpunit inside your test environment, to ensure you’re using the bundled version of PHPUnit (again, possibly just an issue for me- who knows).
  • Write tests which test your own code, not WordPress functions. WordPress unit tests make sure the core functions are always working.

Comments

2 responses to “Adding unit tests to your WordPress plugin using wp-env”

  1. Roel Avatar

    Thanks for this tutorial, this is what I was looking for to start testing my WordPress plugins.

    I quick note to add would be that I also had issues with PHPUnit 10 and Yoast Polyfills, issues like missing files and undefined methods. I had to use PHPUnit 8 (same version as in your starter plugin) and that fixed the problem. Hopefully they can release a fix anytime soon.

    Thank you.

  2. Ethan Clevenger Avatar
    Ethan Clevenger

    Thanks for this write-up!

    You mentioned “I also call /vendor/bin/phpunit instead of simply phpunit as many people (read: probably only me) have a copy of PHPUnit installed globally on their machine”, but I don’t think that’s actually the issue. wp-env should run the copy of PHPUnit included with the Docker container – not a copy you have installed globally.

    However, wp-env doesn’t come with `yoast/phpunit-polyfills` available. So if you’re testing a plugin, you have to include it yourself. And further, the copy of PHPUnit you use needs to know about it.

    A quick `composer require –dev yoast/phpunit-polyfills` will install both the polyfills and its dependency, PHPUnit itself (a compatible version, mind you). At which point, you end up having to do `./vendor/bin/phpunit` anyway to get a copy of PHPUnit that knows anything about the polyfills. Unless you want to tinker with your bootstrap file, but I rather leave the default one that `wp-cli scaffold plugin-tests` gives you.

Leave a Reply

Your email address will not be published. Required fields are marked *