Now that we got the administrative part out of the way let’s look at the code for which we are going to write tests. I included a plugin in the Vagrant setup covered in the last article. This plugin’s intended functionality is as follows:

  • Upon saving a post, it checks the post title for keywords retrieved from an external service.
  • If one of those keywords matches it adds the keyword to the post as a category.
  • If the category term doesn’t exist it creates it before adding it to the post.

The code for this plugin is in /wp-content/plugins/demo-plugin/demo-plugin.php; there are 6 methods:

  • init which is hooked into the WordPress init hook and fires attach_hooks() which makes the plugin work
  • attach_hooks hooks the categorize_post method into WordPress’ save_post hook and performs the action of checking for keywords
  • categorize_post does the majority of our work, it grabs the external service (via the get_listing method and compares the keywords with the saved post’s title, executing the process_terms method when it finds a match.
  • process_term checks if the category term exists, and if necessary fires the create_term method to create it. It then attaches the category term to the post.
  • create_term adds the category term if necessary
  • get_listing makes an external call to our API of keywords.

While six methods may seem like overkill each method has a narrow scope, the single responsibility principle is meant to make our (and our teammates’) lives easier. This helps make the code easier to test, easier to digest and easier to refactor. This should become evident as we write out our tests.

Note that this plugin is solely built as a test and may seem a bit contrived – just keep in mind that the goal of this article is to explain unit testing, not show you the absolute best way to write a plugin.

Testing, Finally

Well, not *just* yet. Before we get started, I wrote tests that cover most of what we need. Feel free to review them or work along with them. Finally it’s time to get started writing tests! Go into the /tests/ directory created by WP-CLI and rename the test file to test-demo-plugin.php. Your tests should match the file names of the files they are testing and should be nested as the files you are working with are nested. This makes finding a particular test trivial. Rename the class to Demo_Test, keep it extending WP_UnitTestCase and clear out its body.

init method

Code we are testing:

function init() {
    $this->attach_hooks();
}

Create a method called test_init and let’s take a look at the method Demo_Plugin::init. This plugin only does one thing, it calls the method attach_hooks. We can test this using a test double or a mock. Mocks can be fairly complex, especially for our first test, but you can think of a mock as a stand in.

The mock takes the place of an object and can check if certain conditions are met, for example, if the object calls a particular method. For this test, we want to create a mock of Demo_Plugin and check if attach_hooks is called when we run Demo_Plugin::init() PHPUnit comes with a built in getMockBuilder that we can use as follows:

$demo_plugin = $this->getMockBuilder( 'Demo_Plugin' )
    ->setMethods( array( 'attach_hooks' ) )
    ->getMock();

What this does is creates a creates a mock object which is a simulated object that mimics the behavior of the real object of the Demo_Plugin class and allows us to check for conditions on the argument of the setMethods method, in this case ‘attach_hooks.’ We get the mocked object by chaining the getMock method to the end.

Now that we have this mock object, we need a way to say, I expect attach_hooks to be called once when the init method is executed. Luckily there’s a super simple way to do this, the syntax is as follows:

$demo_plugin->expects( $this->once() )
    ->method( 'attach_hooks' );

The object returned by the mock builder provides us with a set of methods to use including expects and method. This is saying, stand in class, I expect the method attach_hooks to be called exactly one time during this test. Let’s run the test that we have written and see what our results are.

Note to run the test, execute vagrant ssh from the directory that you ran vagrant up, navigate to /vagrant/wp-content/plugins/demo-plugin/ and then execute phpunit.

It failed, but why? We set up exactly what we expected. One thing that we forgot – and when I started I did this countless times. You have to call the method you are testing in the first place. Our final test should be:

function test_init(){
    $demo_plugin = $this->getMockBuilder( 'Demo_Plugin' )
        ->setMethods( array( 'attach_hooks' ) )
        ->getMock();
    $demo_plugin->expects( $this->once() )
        ->method( 'attach_hooks' );
    $demo_plugin->init();
}

Lo and behold. It passes.
That was pretty complex stuff – let’s quickly review mock objects. If you want to test a method based on conditions we can use PHPUnit’s mock builder. This was a very basic usage of the mock builder – there are also methods to ensure that a method is not called at all, called a specific number of times, called with specific arguments and more.

attach_hooks method

Code we are testing:

function attach_hooks() {
    add_action( 'save_post', array( $this, 'categorize_post' ) );
}

Now that we got a hard one out of the way let’s test the attach_hooks method, which should prove a little easier. Looking at the code it just attaches a single hook. WordPress provides the has_action function that is perfect for testing this: if the action exists, has_action will return the priority on the action for that function, which, by default, is 10.

All we need to do is run the attach_hooks method and check if the method was hooked into save_posts. Since this is straight forward, there’s no mock necessary. This will be our first usage of an assertion. We can assert that the call to has_action returns the expected priority. To do this, we write:

function test_attach_hooks() {
    $demo_plugin = new Demo_Plugin;
    $demo_plugin->attach_hooks();
    $this->assertEquals( 10, has_action( 'save_post', array( $demo_plugin, 'categorize_post' ) ), 'Demo_Plugin::attach_hooks is not attaching Demo_Plugin::categorize_post to save-post' );
}

This test passes as well! Assertions are a fundamental part of unit testing and there’s no shortage of assertions that come out of the box with PHPUnit. Commonly used ones are assertTrue, assertFalse, assertArrayHasKey, assertArrayNotHasKey, assertRegex, assertNotRegExp, as you can see most of the assertions also have negative assertions as well which is very convenient. You can also write your own assertions if necessary.

It’s important to also note that the third argument for assertEquals (it’d be the second for assertTrue, since you are testing just 1 item instead of 2), allows you to put a custom message in the case that the test fails.

categorize_post method

Code we are testing:

function categorize_post( $post_id ) {

    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }

    if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
        return;
    }

    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }

    if ( false !== wp_is_post_revision( $post_id ) ) {
        return;
    }

    $this->post_id = $post_id;
    $title         = get_the_title( $post_id );
    $this->get_listing();
    if ( count ( $this->listing ) ) {
        foreach ( $this->listing as $term_title ) {

            if ( strstr( $title, $term_title ) ) {

                $this->term_title = $term_title;
                $this->process_term();

            }
        }
    }
}

Time for a challenge. We are going to write a test for the categorize_post method. This is going to introduce two new concepts, dataProviders and factories which are both conveniences provided by PHPUnit out of the box and WP_UnitTestCase, respectively.

Data providers give us a shortcut to passing multiple sets of data through a single test, as opposed to writing a different test for each dataset. Factories let us create posts and users (and attachments among other things) so that we can interact with WordPress more dynamically. We need to create a user because of the check made in the categorize_post method:

if ( ! current_user_can( 'edit_post', $post_id ) ) {
    ...
}

So our ‘current user’ has to be able to edit the post, or the method will fail before we can even test it. Our plan of attack is to create an array of keywords pairs that we will assign to Demo_Plugin::listing to test our titles against. Then we will create a posts with their titles fed through the data provider and check how many matches we have (by the number of times the process_terms method is called.

This may sound complex, but when broken down it’s not very complicated. Let’s create a test function with two arguments in it’s signature, this will be the data fed in through the data provider.

function test_categorize_post( $title, $process_term_expects ) {
    ...
}

the $title will be the title of post that we will have the factory create. the $process_term_expects will be the number of matches we expect the title to have with our sample API response. Let’s start out our test creating the post, the user that will have the right permissions, and setting the current user to that user.

$post_id = $this->factory->post->create( array( 'post_title' => $title ) );
$user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
wp_set_current_user( $user_id );

The factory’s implementation for users and posts are very similar. You pass the create method an array of the attributes you want for the factory created post or user. Lastly we set our current user to the freshly minted administrator who will surely have the right permissions to allow us to fully test this method.

Next we mock the Demo_Plugin object we want to create. This time we want to stub out process_term and get_listing methods. The reason for process_term is so that we can count how many times our title matches the keywords we feed the object and get_listing we want to stub out because it makes an API call that we are unconcerned about as far as this test goes.

We will pass sample data to the Demo_Plugin::listing property to run the test with control data.

$demo_plugin = $this->getMockBuilder( 'Demo_Plugin' )
    ->setMethods( array( 'process_term', 'get_listing' ) )
    ->getMock();

This isn’t much different that the test we created for the test_init method. Next we need to create the expectations for these methods. Looking at the class, the get_listing method should only be called once. So just as we did with attach_hooks we do with get_listing.

$demo_plugin->expects( $this->once() )
    ->method( 'get_listing' );

It gets a little tricky with process_term, this should be dynamic, depending on what we feed into the data provider. Luckily we have prepared for this and are passing this into the test_categorize_post method as the second argument. Instead of the once method, we use the exactly method and pass in that argument.

$demo_plugin->expects( $this->exactly( $process_term_expects ) )
    ->method( 'process_term' );

This says, expect the process_term method to run the number of times the data provider tell you to run it. If this doesn’t make sense just yet it should clear up when we go over the data provider. Next lets pass our mock object the data we want to test it against. I chose two keywords and I passed them exactly how the API would pass them (after json_decoding)

$demo_plugin->listing = array( 'Matt Harvey', 'Daniel Murphy' );

Lastly, to complete the method, let’s call categorize_post with the post id which is the required argument.

$demo_plugin->categorize_post( $post_id );

Now we’re not quite done with this, we need to set up the data provider. The format for data providers is a method that returns an array of arrays. The nested arrays should have the same number of elements as arguments passed into the method, which in our case is two ($title, $process_term_expects).

We are going to pass an article title, and how many matches it has to the mocked response we created above. I typically name the method the same as the method I am passing the data with the word provider and an underscore prefixed. Here is the sample data:

function provider_test_categorize_post() {
    return array(
        array( 'Matt Harvey plays catch', 1 ),
                array( 'Everyone Happy that Matt Harvey has successful Bullpen Session', 1 ),
                array( 'Mets Lose, Daniel Murphy Shoulders the Blame', 1 ),
                array( 'Daniel Murphy and Matt Harvey Headline Mets Charity', 2 ),
                array( 'iON offers Mets fans a view never seen before', 0 ),
    );
}

Looking through these you can see that the first three only contain one of the matches. The third contains both of the keywords in my mocked set and the last contains zero. The number of expected matches is the second element of the array. Lastly, we need to tell the tested method that we want to use the provider_test_categorize_post data provider. We do that in the form of a PHPDoc block. The entire method should look as follows:

/**
  * @dataProvider provider_test_categorize_post
  */
function test_categorize_post( $title, $process_term_expects ) {
    $post_id = $this->factory->post->create( array( 'post_title' => $title ) );
    $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
    wp_set_current_user( $user_id );
    $demo_plugin = $this->getMockBuilder( 'Demo_Plugin' )
                ->setMethods( array( 'process_term', 'get_listing' ) )
                ->getMock();
    $demo_plugin->expects( $this->exactly( $process_term_expects ) )
                ->method( 'process_term' );
    $demo_plugin->expects( $this->once() )
                ->method( 'get_listing' );
    $demo_plugin->listing = array( 'Matt Harvey', 'Daniel Murphy' );
    $demo_plugin->categorize_post( $post_id );
}

Run the test and see that all of the tests we’ve written pass. Just to prove that they are working as expected, change the 2 from the fourth array in the data provider to 1 and see how the test fails. Each data provider pass is counted as a single test (dot).

process_term method

Code we are testing:

function process_term() {
    $this->term_object = get_term_by( 'name', $this->term_title, 'category' );

    if ( ! $this->term_object ) {
        $this->create_term();
    }

    if ( is_array( $this->term_object ) ) {

        wp_set_object_terms( $this->post_id, $this->term_object['term_id'], 'category', true );

    }

}

The final two tests are simpler and do not require mocked objects. To test process_term we instantiate Demo_Plugin, create a post using the factory, and set the properties post_id to the post id of the factory created post, term_title to “This is the title” and set term_object to false. Next we run the process_term method and then check if the term exists by using the get_term_by function and asserting that the response is not false, and that the post has the category using the has_category function and asserting that the response is true.

function test_process_term() {
    $demo_plugin              = new Demo_Plugin;
    $demo_plugin->post_id     = $this->factory->post->create();
    $demo_plugin->term_title  = 'This is the title';
    $demo_plugin->term_object = false;
    $demo_plugin->process_term();
    $term = get_term_by( 'name', 'This is the title', 'category' );
    $this->assertNotFalse( $term );
    $this->assertTrue( has_category( $term, get_post( $demo_plugin->post_id ) ) );
}

We could have mocked the method and checked if create_term method was called once, but then we wouldn’t have been able to check that the post had the category because the term would never have been created because of the stub.

create_term method

Code we are testing:

function create_term() {
    $this->term_object = wp_insert_term( $this->term_title, 'category' );
}

Finally, this is a bit overkill because we technically tested create_term in the previous test, however, we can test create_term by instantiating Demo_Plugin, setting the property term_title, calling the create_term method and using the get_term_by function to check if the term was created. As you can see, writing more concise code makes writing tests far easier. Imagine the different cases you would have to account for if you handled the entire plugins functionality in just two methods. Additionally, writing tests is not as complicated as expected, at least they weren’t for me. Next we wrap up this series by going over some more annotations to go along with dataProvider, coverage reports and we’ll you how a unit test can save you time, headaches and a swarm of bug tickets when refactoring.

[voce-related-posts]