Isaías
Great article! It has helped me to implement it with Pest.
When I was getting started in web development, I remember how conceptually overwhelming it was to understand the whys, whats, and hows around things like unit testing. And to make it even more difficult, WordPress was the environment in which I spent most of my time — a platform not well-known for its strong culture of unit testing.
As far as I can tell, the most-recommended resource for unit testing in WordPress is the scaffolding provided by the WP-CLI. Run a command, and it’ll do things like download a copy of WordPress, set up a test database, and provide some nice-to-have methods for integrating your tests with the WP infrastructure itself. The problem is that every time I’ve tried to get this set up — for both themes and plugins — I’ve run into unexpected database or file structure issues. I spend a good share of time working through them, and then maybe have enough mental motivation to write a test.
There’s got to be an easier way to set this stuff up.
I like what the WP-CLI approach has to offer. The helper methods provided by WP_UnitTestCase
(the class you can extend rather than PHPUnit\Framework\TestCase
) alone might make it worth working through the setup friction. But sometimes, you just wanna start writing some friggin’ tests to build some value for your theme or plugin. In those cases, there’s a better way: hook it up yourself. It’s not as scary as it sounds.
For this run-through, I’ll be using my wp-skateboard setup, which runs on Docker, but it really doesn’t matter whether you’re using this, Vagrant, or anything else. But if you are using Docker, make sure you run any of the following commands inside your running container. To help with that, wp-skateboard
allows you to run make bash
to easily enter the container with a bash prompt.
We’ll also be using Composer for package management and autoloading. And because most tutorials I’ve found focus on plugins, we’ll stick there too.
To start, cd
into your plugin, and go through the prompts of the composer init
command. If you already have a composer.json
file in the directory, just move on.
After that, add bit of autoloading data that’ll come in handy later. Here, we tell Composer to automatically include certain files within the tests/
directory (where our tests will live) when they’re namespaced to PluginTests
. A little more on that in a bit.
"autoload-dev": {
"psr-4": {
"PluginTests\\": "tests/"
}
}
Next up, we need to install PHPUnit.
composer require phpunit/phpunit --dev
After this completes, test your installation with by running the following command.
vendor/bin/phpunit
You should see a bunch of information looking something like this:
PHPUnit 8.2.3 by Sebastian Bergmann and contributors.
Usage: phpunit [options] UnitTest [UnitTest.php]
phpunit [options] <directory>
Code Coverage Options:
--coverage-clover <file> Generate code coverage report in Clover XML format
--coverage-crap4j <file> Generate code coverage report in Crap4J XML format
...
Since this is a WordPress plugin, we’ll likely need access to some level of WP core functionality in order to effectively test our code. To make all of that available, we’re going to load WP core during our tests by bootstrapping it in our phpunit.xml
file. Throw some basic configuration into that file, and take special note of the bootstrap
attribute:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="bootstrap.php">
<testsuites>
<testsuite name="WP Unit Testing">
<directory suffix="Test.php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>
Here, we’re loading a bootstrap.php
file, which will be created in our plugin directory, loading two things:
autoload.php
- This is generated by running composer install
, and will take care of the PHP autoloading configuration we added earlier.wp-load.php
- This is a file from WordPress core that’ll load the CMS functionality we’ll need to run our plugin’s code as we test it.<?php
// bootstrap.php
require_once(__DIR__ . "/../vendor/autoload.php");
require_once(__DIR__ . "/../../../../wp-load.php");
With this in place, these two files will be loaded every time our test suite runs, providing us with the resources we need to test.
Also, take note of this line back in our phpunit.xml
file:
<directory suffix="Test.php">./tests/</directory>
What this is doing is specifying the type of named files we’re doing to run as tests within the ./tests
directory. Since that suffix is set to “Test.php,” any file that ends with that string will be run.
At this point, try running vendor/bin/phpunit
again. This time, you should see something that looks more like this:
PHPUnit 7.3.5 by Sebastian Bergmann and contributors.
Time: 1.03 seconds, Memory: 22.00MB
No tests executed!
Create a tests
directory in your plugin, and inside that, a SomeClassTest.php
file. Remember, that file name is important. Now, let’s just verify that we can get a test to run without issue. Throw some meaningless test in there. A method is deemed a test when its name starts with test
.
<?php
class SomeClassTest extends \PHPUnit\Framework\TestCase
{
public function test_shouldWork()
{
$this->assertTrue(true);
}
}
Running vendor/bin/phpunit
should produce something like the following:
PHPUnit 8.2.3 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 2.59 seconds, Memory: 54.25 MB
Noice.
From here on out, we can start leveraging the WordPress ecosystem as needed. For a stupid example, let’s say we have a method that’ll retrieve the content of a post in an emotionalized based on the tags attached to the post. Specifically, we’ll format the content differently if the post is tagged angry
, excited
, or obnoxious
.
<?php
/*
Plugin Name: Unit Testing Plugin
*/
function getEmotionalizedContent($postId)
{
$content = get_the_content(null, false, $postId);
if (has_tag('angry', $postId)) {
$content = strtoupper($content);
}
if (has_tag('excited', $postId)) {
$loudContent = array_map(function ($character) {
if ($character === ".") {
return str_repeat("!", rand(1, 10));
}
return $character;
}, str_split($content));
$content = implode($loudContent);
}
if (has_tag('obnoxious', $postId)) {
$content = str_replace(" ", " 👏 ", $content);
}
return $content;
}
Because our PHPUnit configuration is set to import WordPress core, our tests will have access to this plugin (make sure it’s active in the WP admin!), as well as everything else loaded by WordPress’ wp-load.php
file. So, we should be ready to dive in & test.
First off, let’s add some code to SomePostClass
that’ll provide us with a post to manipulate and clean up after we’re finished.
class SomePostClass extends \PHPUnit\Framework\TestCase
{
public $testPostId;
protected function setUp(): void
{
$this->testPostId = wp_insert_post([
'post_title' => 'Sample Post',
'post_content' => 'This is just some sample post content.'
]);
}
protected function tearDown(): void
{
wp_delete_post($this->testPostId, true);
}
//... tests will go here.
}
Leveraging setUp()
and tearDown()
, we’re creating a fresh WP post and deleting it after any given test method is complete. Since we’re passing true
to wp_delete_post
, it’ll also clean up any attached post meta we create along the way.
After that, we can add our method-specific tests to cover all of our cases.
public function test_getEmotionalizedContent_shouldBeAngryWhenTagIsApplied()
{
wp_set_post_tags($this->testPostId, 'angry', true);
$this->assertEquals(
getEmotionalizedContent($this->testPostId),
"THIS IS JUST SOME SAMPLE POST CONTENT."
);
}
public function test_getEmotionalizedContent_shouldBeExcitedWhenTagIsApplied()
{
wp_set_post_tags($this->testPostId, 'excited', true);
$this->assertStringEndsWith(
"!",
getEmotionalizedContent($this->testPostId)
);
}
public function test_getEmotionalizedContent_shouldBeObnoxiousWhenTagIsApplied()
{
wp_set_post_tags($this->testPostId, 'obnoxious', true);
$this->assertEquals(
getEmotionalizedContent($this->testPostId),
"This 👏 is 👏 just 👏 some 👏 sample 👏 post 👏 content."
);
}
public function test_getEmotionalizedContent_shouldBeEverythingWhenTagAreApplied()
{
wp_set_post_tags($this->testPostId, 'obnoxious', true);
wp_set_post_tags($this->testPostId, 'excited', true);
wp_set_post_tags($this->testPostId, 'angry', true);
$result = getEmotionalizedContent($this->testPostId);
$this->assertStringStartsWith(
"THIS 👏 IS 👏 JUST 👏 SOME 👏 SAMPLE 👏 POST 👏 CONTENT",
$result
);
$this->assertStringEndsWith("!", $result);
}
Running vendor/bin/phpunit
again, you should see some success:
root@bc37c28f5d63:/var/www/html/wp-content/plugins/unit-testing-plugin# vendor/bin/phpunitPHPUnit 8.2.3 by Sebastian Bergmann and contributors.
.... 4 / 4 (100%)
Time: 3.44 seconds, Memory: 32.00 MB
OK (4 tests, 5 assertions)
Gr8 work. But as your tests grow, it’s likely that you’ll need to interact with posts throughout several different classes. So, let’s abstract a bit of this WordPress-specific stuff out into its own class that extends \PHPUnit\Framework\TestCase
.
<?php
namespace PluginTests;
class PostTestCase extends \PHPUnit\Framework\TestCase
{
public $testPostId;
public function setUp(): void
{
$this->testPostId = wp_insert_post([
'post_title' => 'Sample Post',
'post_content' => 'This is just some sample post content.'
]);
}
public function tearDown(): void
{
wp_delete_post($this->testPostId, true);
}
}
Then, in our test class, we can extend this new helper class:
-class SomePostTest extends \PHPUnit\Framework\TestCase
+class SomePostTest extends \PluginTests\PostTestCase
Thanks to that bit of autoloading configuration we did earlier, we don’t need to include our new helper class — it happens automatically, out of sight. And as a result, from here on out, we can easily access $this->testPostId
and any other resource we choose to make available in PostTestCase
, rather than handling all of the setup and cleanup within our individual test classes.
As the need arises, you can create your own different test cases from which to extend, perhaps based on the type of resource you need. For example, a \PluginTests\UserTestCase
might be good to have in order to easily test code pertaining to WordPress user objects.
The value this entire approach gives is (hopefully) a quicker path to start writing tests in WordPress. That said, there are some trade-offs that come along it. For example:
If you’ve got some tips to improve this whole setup without losing its relative simplicity, share them! At any rate, hope this is helpful.
Alex MacArthur is a software engineer working for Dave Ramsey in
Nashville-ish, TN.
Soli Deo gloria.
Get irregular emails about new posts or projects.
No spam. Unsubscribe whenever.Great article! It has helped me to implement it with Pest.
Glad to hear it, Isaías!
Exactly what I was looking for! I didn't want all the unnecessary bloat that comes from the wp-cli approach. thanks!
Glad to hear it helped!
Really helpful article. Good to keep things simple and this gives just enough power to write meaningful tests without going overboard.
Hi Alex, thanks so much for your article giving me some ideas to do better unit testing things. Have a good day.
You miss out on the features included with the WordPress-recommended test suite setup.
Can you go into more detail? What sort of features?
Thank you very much for the post!