Want to get more articles like this one? Join my newsletter

Introduction to WordPress unit testing

I gave a talk at WordCamp Toronto 2015 on WordPress unit testing. This is the companion article that I wrote for it. If you’re just looking for the slides, click here.

“But this worked the other day!”

I don’t know about you, but reading that sentence just frustrates me! Don’t you hate when things worked one day and it doesn’t the next. It’s the stuff of (developer) nightmares.

What if there was a way to ward yourself against that evil? Well, you’re in luck because that’s the goal of software testing! It prevents this situation from happening over and over again. On top of that, it helps you improve the quality of the code you write. Awesome!

Now, software testing is a HUGE field. You have a ton of different types of testing. Each with its own purpose (and, sometimes, philosophy). Today, we’re going to focus on just one type of testing. It’s called unit testing.

So what is unit testing?

Well, in the full spectrum of testing, unit testing focuses on testing at the smallest scale. In most cases, that means inputting values into a function (or object method). You then check at what comes out at the other end.

“Checking what comes out at the other end” tends to boil down to two things. You can verify that the function returned the proper value. Or you can look for errors when you supply invalid input.

The ultimate goal is to ensure that the behaviour of our function stays consistent. You can make changes to it knowing that there’s a safety net that’ll catch you if you make a mistake. This ends up improving the quality of your work in the long run.

The importance of isolation in unit testing

Most types of testing need some amount of isolation. That said, nowhere is this more important than in unit testing. Unit testing, by its name, is about isolating a unit of code for testing.

Isolating a unit of code is a lot like hooking your function to a set of magical machines. They let you control everything that surrounds your function. In the testing world, we call these machines test doubles.

For example, let’s say that your function calls apply_filters. A test double can check if your function passed it the correct value. It can also return a value that you choose to your function.

This lets you focus on just testing the internal logic of the function. You don’t have to worry about what the rest of the application (WordPress here) is doing. It’s all under your control.

What makes testable code higher quality

Now, this doesn’t mean that you can’t have high-quality code without tests. Of course, you can. But why is it that you often hear something like “Testable code is higher quality code.”

The answer lies with something called cyclomatic complexity. It’s a metric that evaluates how complex a function is. It does so by calculating how many independent paths there are through your code.

We won’t be diving into it more than that since it’s just a lot of math (boring!). That said, it has an important takeaway that applies here. In general, the larger a function gets, the more complex it becomes. And the more complex a function is, the harder it is to test it.

Here’s an example. Let’s say you have a function that has hundreds of lines of code. It has plenty of conditional statements. That means that you have dozens of possible paths through it.

Next, imagine that you had to create tests for all these paths. The tests themselves need to be more complex. That’s because your function might call a bunch of other functions as well. All these factors turn isolating your code and controlling the inputs into a nightmare.

This pushes you towards writing smaller, less complex functions. These functions are easier to isolate and need fewer inputs. They also tend to be of higher quality. That’s because they focus on doing a specific job and doing it well.

The difference between unit tests and WordPress tests

Often, you’ll see WordPress tests called WordPress unit tests. This isn’t quite right. In the testing spectrum, WordPress tests are what we call integration tests.

The point here isn’t to argue semantics. There’s an actual difference between the two that’s worth highlighting. It’s their relationship with isolation.

Integration tests don’t care that much about isolating a piece of code. They only care about how various pieces of code work together as part of a larger whole. You might need to use isolation to do that, but it isn’t necessary most of the time.

That’s all well and good, but which of the two is better then?

Neither is better than the other. In fact, the best is to do both. You start by using unit tests to test the behaviour of your code. You then use WordPress tests to test how your code behaves within WordPress.

That said, this is a topic for another time. You’ll see that you already have plenty on your plate with unit testing. Let’s get started.

The unit testing setup

Before we start building our first unit tests, we need to make sure that you have the right setup. This next section will go over all that. We’ll go over the requirements and the tools necessary for running unit tests.

Operating system

Let’s start with the operating system. A lot of the tools that we’ll use are only available on UNIX-based operating systems. That’s another way to say anything, but Windows. But all isn’t lost for you Windows users out there!

You can still write unit tests for your plugin. You’ll just need a bit of help. You can use a virtual machine to run the tests on your computer. You can also use an automated testing service to do run them for you.

Required PHP versions

To create test doubles of WordPress functions, we need to use namespaces. This means that your plugin will need to use PHP version 5.3 or later.

We’re also going to use a second testing library besides PHPUnit. This testing library uses a trait to add functionality to our tests. This will limit the coverage of your automated tests (a topic for another time) to PHP 5.4 and later.

Composer

We’ll need to use composer for our testing setup. Composer is a package manager for PHP. It lets you specify external libraries that your plugin needs to work.

A lof of plugin developers use it to download external libraries that they use for testing. This is also what we’ll be doing. We’ll use it to install this secondary testing library.

If you’ve never installed Composer before, you can follow the installation instructions here.

PHPUnit

PHPUnit is one of the testing library used in the PHP world. For a long time, it was also the only option available. While it’s not anymore, it’s still the testing library used by a lot of PHP projects such as WordPress.

If you’ve never installed PHPUnit before, you can follow the installation instructions here.

WP-CLI (optional)

WP-CLI is the WordPress command-line tool developed by the community. This isn’t a strict requirement for unit testing a plugin. We’re just going to use WP-CLI to speed up some tasks around our unit tested plugin.

If you’ve never installed WP-CLI before, you can follow the installation instructions here.

Creating our unit tested plugin

We’ve got our unit testing setup ready. It’s time to get to work building our unit tested plugin.

Create our plugin

First, we need to create our plugin. The easiest way to do that is with WP-CLI. You just need type in one command.

wp scaffold plugin unit-tested-plugin --plugin_name="WordPress Unit Test Demo"

This is all done using the scaffold plugin command. It creates our plugin with all the necessary plugin files to get going. On top of that, it’ll generate a lot of the files that we need to run our unit tests.

Add composer.json file

WP-CLI doesn’t create the composer.json file for our plugin. This JSON file describes our plugin to Composer. We only need this file to tell Composer what external libraries our plugin wants to use.

{
    "name": "carlalexander/wordpress-unit-test-demo",
    "type": "project",
    "description": "Demonstrates how to build unit tests with WordPress",
    "homepage": "https://carlalexander.ca",
    "license": "GPL-3.0+",
    "require": {
        "php": ">=5.3.0"
    },
    "require-dev": {
        "php-mock/php-mock-phpunit": "^0.3"
    }
}

Here’s our composer.json file. For the sake of completeness, there’s a bit more than just our requirements in it. The first part of the file describes our plugin a bit. We give it a name, a description and a license. The two entries that we care about are require and require-dev.

require tells Composer the general requirements of our plugin. For our plugin, that’s just the PHP version. As we discussed earlier, we need PHP 5.3 to use namespaces in our plugin.

require-dev tells Composer the development requirements of our plugin. These are extra requirements on top of the ones in require. This is where we’ll add the requirement for our secondary testing library: PHP-Mock.

This is why we need Composer in our setup. PHP-Mock is the library that will let us create test doubles of WordPress functions. We’ll be using that library to build all our unit tests.

Modify bootstrap.php

bootstrap.php is a file that PHPUnit will load before running our tests. It’s where you put your extra configuration options. It’s also where you want to load external libraries like WordPress.

WP-CLI created bootstrap.php when we ran the scaffold plugin command. Since WP-CLI doesn’t support Composer, the bootstrap file won’t load anything that Composer downloaded. We need to fix that.

# tests/bootstrap.php

require_once(dirname(__DIR__) . '/vendor/autoload.php');

$_tests_dir = getenv('WP_TESTS_DIR');
if ( !$_tests_dir ) $_tests_dir = '/tmp/wordpress-tests-lib';

require_once $_tests_dir . '/includes/functions.php';

function _manually_load_plugin() {
    require dirname( __FILE__ ) . '/../unit-tested-plugin.php';
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );

require $_tests_dir . '/includes/bootstrap.php';

We added that require_once statement at the top of the file. It tells PHPUnit to load the autoloader file created by Composer. This is what will let us use the PHP-Mock library (and any other external libraries) in our tests.

Adding our first plugin function

We’re almost ready to create our unit test. There’s only one problem left. It’s that we no code to test! Our plugin file sits empty in our plugin directory. We need to rectify that situation.

# unit-tested-plugin.php

namespace UnitTestDemo;

/**
 * Get a plugin option from the WordPress database.
 *
 * @param string $name
 *
 * @return mixed
 */
function demo_get_option($name)
{
    return get_option('demo_' . $name);
}

You’ll notice that we added a namespace at the top of the file. This is a necessary step to create our unit tests. Without it, we can’t create our test doubles.

demo_get_option is a helper function around get_option. It helps us namespace our plugin options. That way, we don’t have worry about adding the demo_ prefix all the time.

Creating your first unit test

Cool! Now that our plugin is ready for testing. It’s time to create our first unit test.

Creating our empty test class

The first thing we need is a class to contain our tests. That’s how testing works with PHPUnit. You group all your relevant tests together inside the same class.

# tests/test-demo.php

namespace UnitTestDemo;

use phpmock\phpunit\PHPMock;

class DemoTest extends \PHPUnit_Framework_TestCase
{
    use PHPMock;

    // ...
}

There are a few things about our DemoTest class that’s worth discussing here. First one is that we’re using the same UnitTestDemo namespace as our demo_get_option. This keeps our example a bit simpler.

There’s also the use phpmock\phpunit\PHPMock; statement. It imports the PHPMock trait into our namespace. This will let us use it in our test class.

Speaking of our test class. Our DemoTest class isn’t extending theWP_UnitTestCase class. That’s what you’d see in the usual WordPress test examples. Instead, we’re extending PHPUnit_Framework_TestCase which is the default class used with PHPUnit. The \ in front of PHPUnit_Framework_TestCase signifies that it’s in the global namespace.

There’s also use PHPMock; statement at the beginning of our class. That’s the trait from the PHP-Mock library. A trait is a way to add extra functionality to our class. It contains the function that we’ll use to create our test doubles.

Testing demo_get_option

Let’s move on to writing a unit test for demo_get_option. I want to test that, if I request an option, I get the right value back. So let’s do that.

# tests/test-demo.php

namespace UnitTestDemo;

use phpmock\phpunit\PHPMock;

class DemoTest extends \PHPUnit_Framework_TestCase
{
    use PHPMock;

    public function test_demo_get_option()
    {
        $this->assertEquals('bar', demo_get_option('foo'));
    }
}

test_demo_get_option is our test function. Prefixing our method with test flags our method as a test that PHPUnit needs to run. The second part of the method name is a description of the test that we want to do.

assertEquals is how we’ll check that we get the right value back. PHPUnit calls these types of methods assertions. PHPUnit supports a lot of different assertions.

The first argument of assertEquals is 'bar'. That’s the value we expect to get back. The second argument is our call to demo_get_option with the argument foo. This fetches the demo_foo option from the test database.

Let’s try to run our test and see what we get.

Screen Shot 2015-10-01 at 4.10.01 PM

Oops, it looks like our test is failing! Why is that? It’s because we never created our test double for get_option. Right now, it uses the get_option function from WordPress.

Since there’s no demo_foo option in our test database, get_option can’t return 'bar'. Instead, it returns its default value which is false. That’s why the error says: “Failed asserting that false matches expected ‘bar’.” Let’s fix that!

Creating our test double

The normal reaction to our error is to create a demo_foo option in our test database. That would let us test that demo_get_option is working. That said, we won’t be doing that. Instead, we’re going to create a test double. This will let us test the code inside demo_get_option in isolation.

# tests/test-demo.php

namespace UnitTestDemo;

use phpmock\phpunit\PHPMock;

class DemoTest extends \PHPUnit_Framework_TestCase
{
    use PHPMock;

    public function test_demo_get_option()
    {
        $get_option = $this->getFunctionMock('UnitTestDemo', 'get_option');

        $this->assertEquals('bar', demo_get_option('foo'));
    }
}

getFunctionMock is the method that PHPMock trait imported into our DemoTest class. It takes two arguments: our UnitTestDemo namespace and the name of a function. Here, we want it to create a mock of the get_option function. So get_option is our second argument.

Mocking functions or methods isn’t something we’ve talked about so far. A mock (or mock object) is a specific type of test double. Its purpose is to simulate the behaviour of an existing function or object. This lets us verify how our function or method interacts with them. This is the only type of test double that allows us to do this type of verification.

# tests/test-demo.php

namespace UnitTestDemo;

use phpmock\phpunit\PHPMock;

class DemoTest extends \PHPUnit_Framework_TestCase
{
    use PHPMock;

    public function test_demo_get_option()
    {
        $get_option = $this->getFunctionMock('UnitTestDemo', 'get_option');
        $get_option->expects($this->once())
                   ->with($this->equalTo('demo_foo'))
                   ->willReturn('bar');

        $this->assertEquals('bar', demo_get_option('foo'));
    }
}

This is what our configured get_option mock looks like. We used three different methods to configure the expected behaviour. Let’s go over them.

First, we have expects($this->once()). It tells the get_option mock that we expect demo_get_option to call it once.

When it does call it, it’ll pass demo_foo as the option name. That’s what with($this->equalTo('demo_foo')) checks. It validates that demo_get_option prefixes option names with demo_.

The last thing we did is configure the return value. willReturn('bar') tells our get_option mock to return 'bar'. This will only happen if demo_get_option called get_option with the correct value(s).

Screen Shot 2015-10-01 at 4.10.32 PM

Congratulations! You have your first passing unit test. That said, we won’t leave it at that.

Pushing things further

It’s common for plugins and themes to store an array inside an option. But this expectation of receiving an array can be deceitful in practice. You’ll code always expecting an array which often means that your code will break if you don’t get one.

That’s why you’ll often see (array) next to get_option in those situations. This casts the value returned by get_option as an array. Instead of having to think of casting our option each time, we’ll get demo_get_option to do it for us.

We’ll work at it backwards this time. We’ll start by creating a test that fails with our expected outcome. Using that failing test, we’ll change our demo_get_option function. This will give you a taste for test-driven development.

Adding a default value to demo_get_option

But first, we’ll need to make some changes to demo_get_option. That’s because our first pass at it didn’t include support for a default value. We’ll need it to create our improved function.

# unit-tested-plugin.php

namespace UnitTestDemo;

/**
 * Get a plugin option from the WordPress database.
 *
 * @param string $name
 * @param mixed  $default
 *
 * @return mixed
 */
function demo_get_option($name, $default = null)
{
    return get_option('demo_' . $name, $default);
}

As you can see, we added default as a second argument. To change things a bit, we made the default value null instead of false. We pass that value into get_option as a second argument.

Updating our first test

We also need to make changes to our first test. We now have a second argument to verify using the with method. Let’s make the change.

# tests/test-demo.php

namespace UnitTestDemo;

use phpmock\phpunit\PHPMock;

class DemoTest extends \PHPUnit_Framework_TestCase
{
    use PHPMock;

    public function test_demo_get_option()
    {
        $get_option = $this->getFunctionMock('UnitTestDemo', 'get_option');
        $get_option->expects($this->once())
                   ->with($this->equalTo('demo_foo'), $this->identicalTo(null))
                   ->willReturn('bar');

        $this->assertEquals('bar', demo_get_option('foo'));
    }
}

You’ll notice that we’re using identicalTo(null) and not equalTo(null). There’s an important difference between the two. equalTo does a check using == while identicalTo does its check using ===.

=== is what we call a strict comparison operator. This will prevent our test from passing with incorrect values. That’s because values like false or '' are equal to null when using equalTo.

Creating a failing test

Now that we’ve updated our demo_get_option function, we can create our failing test. Our test will check the behaviour of demo_get_option with an array as a default value. We want it to always return an array in that situation.

# tests/test-demo.php

namespace UnitTestDemo;

use phpmock\phpunit\PHPMock;

class DemoTest extends \PHPUnit_Framework_TestCase
{
    use PHPMock;

    public function test_demo_get_option_casts_array()
    {
        $get_option = $this->getFunctionMock('UnitTestDemo', 'get_option');
        $get_option->expects($this->once())
                   ->with($this->equalTo('demo_foo'), $this->identicalTo(array()))
                   ->willReturn('bar');

        $this->assertEquals(array('bar'), demo_get_option('foo', array()));
    }
}

You might have noticed that test_demo_get_option_casts_array is almost the same as test_demo_get_option. That’s often the case when unit testing. The difference between tests comes down to changing the inputs, outputs and expected values.

So what changed here? Well, we changed the default value for demo_get_option to array(). We also updated our get_option mock to reflect that change. That said, our mock still returns 'bar'.

This simulates a situation where get_option found an option in the database. The problem is that the returned value isn’t an array. This is the critical aspect of our test.

We don’t want to get 'bar' back from demo_get_option. We want to get array('bar') even if the value from get_option was 'bar'. That’s what assertEquals is looking for.

Screen Shot 2015-10-01 at 4.27.25 PM

Right now, this doesn’t work. demo_get_option just returns the value returned by get_option. It doesn’t consider that we might not want it as is. We need to change that.

Smart-casting an option as an array

To fix our failing test, we need to compare the value we get from get_option with our default value.

# unit-tested-plugin.php

namespace UnitTestDemo;

/**
 * Get a plugin option from the WordPress database.
 *
 * @param string $name
 * @param mixed  $default
 *
 * @return mixed
 */
function demo_get_option($name, $default = null)
{
    $option = get_option('demo_' . $name, $default);

    if (is_array($default) && !is_array($option)) {
        $option = (array) $option;
    }

    return $option;
}

This is what our updated demo_get_option function does now. We store the value from get_option in the option variable. We then compare it to our default value.

To do that, we use is_array to first detect if default is an array. If it is, we check the value returned from get_option as well. If that value wasn’t an array, we cast it as an array. This ensures that our option is always an array when we pass a default value that is an array.

Screen Shot 2015-10-01 at 4.27.59 PM

And there you go! Both our tests are passing now.

Building your testing habit

At this point, you might be thinking that this was a lot of work for a debatable benefit. That’s a common feeling when you start writing tests. A test, by itself, feels pretty insignificant.

But you have to keep the big picture in mind. That test is just one loop in your safety net. You need a lot of them before you can feel like there’s something there to support you.

That’s why testing is a lot like picking up a new healthy habit. It’s hard to stay motivated when you start. (Chips are soooo tasty!) There seems to be an endless source of seductive objections looking to convince you not to do it:

  • “Why write tests when I could write cool code instead!?”
  • “There’s no way this is going to break!”
  • “We’re going to miss this deadline if we write tests.”

You’ll have to overcome those to develop your testing habit. If you’re ready to start, you can use the code from today as your base. It’s all available in this GitHub repo.

Slides

Here are the slides for the talk I gave at WordCamp Toronto 2015.

Creative Commons License