Unit Testing in WordPress – Testing integrations

I recently wrote a post about setting up PHPUnit tests for WordPress plugins. A colleague where I work at (the most excellent Angry Creative) suggested I provide a more concrete example of how to implement Unit Tests with WordPress itself and even other plugins. Here you go!

If you haven’t read that post yet, you should probably start there.

Integrating PHPUnit Testing with WordPress

When testing integration with WordPress, a good rule of thumb is that you don’t need to test WordPress’s own APIs. WordPress is responsible for this via their own test suite. In other words, in your test class you don’t need to do this:

class MyPluginTests extends WP_UnitTestCase {
    
    /** You don't need to do this! */
    function test_something_for_no_good_reason() {
        $post_title = 'Encounter at Farpoint';
        $post_id    = wp_insert_post([
            'post_title'  => $post_title,
            'post_status' => 'publish'
        ]);

        $this->assertInternalType( 'int', $post_id );

        $post = get_post( $post_id );
        $this->assertEquals( $post_title, $post->post_title );
    }

}

Having said that, I’ve recently written tests for a plugin that imports data via a CSV file using WordPress’s APIs. In this case, I think tests are very useful to make sure that all of the data I want to import is actually imported.

Here’s an example of what I mean.

class MyPluginTests extends WP_UnitTestCase {

	/**
	 * @return array
	 */
	function data_provider() {
		return [
			'title'   => 'Babylon 5',
			'country' => 'Sweden',
			'city'    => 'Malmö',
			'address' => 'Studentgatan 4, 211 38 Malmö, Sweden',
		];
	}

	/**
	 * Test the entire data import flow.
	 */
	function test_insert_post() {
		$data     = $this->data_provider();
		$importer = new Bulk_Importer( $data );
		$post_id  = $importer->init();

		// Check the taxonomies have successfully added and attached to the post
		$countries = wp_get_post_terms( $post_id, 'country' );
		$this->assertEquals( count( $countries ), 1 );
		$this->assertEquals( $countries[0]->name, $data['country'] );

		$city = wp_get_post_terms( $post_id, 'city' );
		$this->assertEquals( count( $city ), 1 );
		$this->assertEquals( $city[0]->name, $data['city'] );

		// Check the post meta has been successfully saved
		$location = get_post_meta( $post_id, 'retailer_position', true );
		$this->assertArrayHasKey( 'address', $location );
		$this->assertArrayHasKey( 'lat', $location );
		$this->assertArrayHasKey( 'lng', $location );
		$this->assertNotEmpty( $location['lat'] );
		$this->assertNotEmpty( $location['lng'] );
		$this->assertEquals( $location['address'], $data['address'] );
	}

}

It’s not really necessary to show the ‘Bulk_Importer’ class here, but suffice to say it adds the data in the ‘data_provider’ method to the database.

The city and country fields are added as terms in 2 separate taxonomies, respectively ‘country’ and ‘city’. The address is geo-encoded via the Google Maps API and that’s handy to have tested, just to make sure my plugin logic is working the way I thought it should. The post_meta ‘retailer_position’ should look like this after it’s been imported:

[
    'address' => 'Studentgatan 4, 211 38 Malmö, Sweden',
    'lat'     => '55.6033432',
    'lng'     => '13.0050809',
];

In my tests I check that the metadata has been saved in the correct format and that the respective taxonomy terms have been appended. I can check the address is the same, but I don’t need to check the result of the Google Maps API. They are responsible for their own APIs!

Integrating PHPUnit Testing with other plugins

When you scaffold your plugin tests via WP-CLI you’ll notice a bunch of files get added to your plugin. If you look at the file at tests/bootstrap.php you’ll see something like this:

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

// 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__ ) ) . '/your-plugin.php';
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );

// Start up the WP testing environment.
require $_tests_dir . '/includes/bootstrap.php';

Note the ‘_manually_load_plugin()’ function. If you need to load additional plugins when you run your tests just add them to the list and they be autoloaded when you run your tests.

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

Now in my tests i can use WooCommerce’s APIs without issue.

class MyPluginTests extends WP_UnitTestCase {

    /**
     * Test that relies on WooCommerce
     */
    function test_woocommerce() {
        // Assuming the product actually exists :)
        $product_id = 666;
        WC()->cart->add_to_cart( $product_id );

        $this->assertNotEmpty( WC()->cart->cart_contents );
    }

}

Again remember that we don’t actually need to test WooCommerce’s APIs. They are responsible for that, this is just to demonstrate that the APIs are in fact available to us.

Setting up data before we run our tests

Often when we want to test our plugin code we will need to create actual data to work with (see the above example). You’ll probably have noticed that our plugin tests class extends WP_UnitTestCase and not PHPUnit\Framework\TestCase.

WP_UnitTestCase class has a bunch of factory objects that you can use for creating posts, attachments, terms, users, comments, blogs, categories, terms and even networks. There doesn’t seem to be any official documentation about the factory classes which is a bit rubbish if you ask me, but google turned up this gist that has some data at least.

Using the setUp and tearDown methods

In your tests class there are 2 methods that will be called before and after each test method in the class is run, namely setUp and tearDown. The setUp method is a good place to use the respective factory object methods to create any data you’ll need in your tests. You can use the factory objects to go nuts and add a bunch of data to your test database here.

class MyPluginTests extends WP_UnitTestCase {

    function setUp() {
         // Call the setup method on the parent or the factory objects won't be loaded!
        parent::setUp();

        // Accepts the same arguments as wp_insert_post
        $post_id = $this->factory->post->create([
            'post_type' => 'page',
            'post_title => 'The name of the place is Babylon 5.',
        ]);

        // Create a bunch of posts, just to have some data to play around with.
        $post_ids = $this->factory->post->create_many( 50 );

        // Create an admin user
        $user_id = $this->factory->user->create([
            'user_login' => 'test',
            'role'       => 'administrator'
        ]);

        // Create a bunch of users
        $user_ids = $this->factory->user->create_many( 4 );

        // Create a single term
        $term_id = $this->factory->term->create([
            'name'     => 'Babylon 5',
            'taxonomy' => 'category',
            'slug'     => 'babylon-5'
        ]);

        // Create a bunch of terms
        $term_ids = $this->factory->term->create_many( 10 );
    }


}

Of course, you can use the factory methods anywhere in your class.

class MyPluginTests extends WP_UnitTestCase {

    // Remember to always call the parent::setUp method to make the factory objects available.
    function setUp() {
        parent::setUp();
    }


    function test_something() {
        $post_id = $this->factory->post->create([ 'post_title' => 'Trees and people used to be good friends' ]);

        $post = get_post( $post_id );
        // Go ahead! Test something :)
    }

}

#protip – if you’re using custom custom taxonomies that are not created the plugin you’re testing you can create them in the setUp method so that they’re available in your tests.

class MyPluginTests extends WP_UnitTestCase {

    function setUp() {
        parent::setUp();

        register_post_type( 'book' );
        register_taxonomy( 'genre', 'book' );

        ...
    }

}

Real world example

Ok, here’s a part of some actual tests i wrote to test a discount plugin I wrote for WooCommerce. The setUp method creates the products and adds them to the cart. The tearDown method removes the products from the cart and deletes them from the database.

class MyPluginTests extends WP_UnitTestCase {

    /** @var array */
    public $products = [];

    /**
     * SetUp
     */
    public function setUp() {
        parent::setUp();

        $this->createProducts();
        foreach ( $this->products as $product_id ) {
            WC()->cart->add_to_cart( $product_id );
        }

    }

    /**
     * TearDown
     */
    public function tearDown() {
        WC()->cart->empty_cart( true );

        foreach ( $this->products as $product_id ) {
            wp_delete_post( $product_id, true );
        }
    }


    /**
     * Create the products.
     */
    public function createProducts() {
        $products = [
            [
                'title' => 'Product 500',
            ],
            [
                'title' => 'Product 400',
            ],
            [
                'title' => 'Product 300',
            ],
        ];

        foreach ( $products as $product ) {
            $this->products[] = $this->factory->post->create([
               'post_title'  => $product['title'],
               'post_type'   => 'product',
               'post_status' => 'publish',
            ]);

            // Plus a bunch more stuff to actually create a valid WooCommerce product.
            ...
         }

    }

    /**
     * WC()->cart now contains the 3 products I created, so I can do some tests on
     * manipulating the cart with 'real' data using the WC_Cart APIs.
     */
    function test_the_cart() {
        ...
    }

}

Awesome brah, now we can test ALL OF THE THINGS!!

Ping me at @richardsweeney if you have any questions about PHPUnit testing in WordPress. I’d be happy to help out if I can :)

Unit Testing in WordPress

Unit testing in PHP is one of those things that people talk about but can be difficult to get your head around until you take the leap and actually write your first test.

It can also be difficult to see the value in taking the time to write tests if you’ve never done it before, but take my word for it – it’s one of those things that when you start writing tests you’ll wonder why the hell it took you so long to start writing tests and how you ever managed before!

I can pretty much guarantee you it will make you change the way you structure your code too, in my case very much for the better.

Getting started

First you’ll need to install PHPUnit.

composer global require phpunit/phpunit

The easiest way to add tests to a plugin is to use WP-CLI.

wp scaffold plugin-tests {plugin-name}

Check out the documentation for more details about this command.

When you’ve run the command, cd to your plugin directory and run.

./bin/install-wp-tests.sh {db-name} {db-user} {db-pass} [db-host] [wp-version] [skip-database-creation]

Just run something like the following:

./bin/install-wp-tests.sh wordrpress_tests mysql_username mysql_password

To install the test library and the tests database. Do not use a real database for this as it will be deleted when you run your tests!!

Writing your tests

Alright, you’ve made it this far! You’ll notice a couple of files added to your plugin now. Check out the default tests class at: tests/tests-sample.php. It should look like this:

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

    /**
     * A single example test.
     */
    function test_sample() {
        $this->assertTrue( true );
    }
}

If you run

phpunit

from your plugin directory you should see something like the following:

Unit tests result

Awesome brah!! Now you just need to write some actual tests. Sitepoint has a pretty good introduction article about this. I suggest just playing around with the API it feels natural.

Try adding the following to your tests class and see what happens :)

function test_something() {
    $string = 'Nice :)';
    $this->assertEquals( 'Nice :)', $string );
}

function test_something_that_fails() {
    $string = 'Nice :)';
    $this->assertEquals( 'BAD :(', $string );
}

There’s a bunch of assertions you can use depending on your use case. They’re all detailed on the PHPUnit site.

Happy testing!

Update! I’ve since written a sort of part 2 to this post that goes a bit deeper into how to integrate your tests with WordPress and even other plugins. Check it out, if you fancy diving a wee bit deeper into the subject.

Add a category to media items in WordPress

If you want to add categories to images and/or other uploaded files in your WordPress media library (in versions 3.5 and above) it’s really easy — once you know how. First register the taxonomy (I’m just using the built in ‘category’ taxonomy type in WordPress, but you could be all fancy and add your own taxonomy if you really want to).

/** Register taxonomy for images */
function olab_register_taxonomy_for_images() {
    register_taxonomy_for_object_type( 'category', 'attachment' );
}
add_action( 'init', 'olab_register_taxonomy_for_images' );

Now add the category field to the Media Library table. This will also add a filter to the table to allow search of attachments by category. Super useful if you’ve got a bunch of files you need to categorize.

 /** Add a category filter to images */
function olab_add_image_category_filter() {
    $screen = get_current_screen();
    if ( 'upload' == $screen->id ) {
        $dropdown_options = array( 'show_option_all' => __( 'View all categories', 'olab' ), 'hide_empty' => false, 'hierarchical' => true, 'orderby' => 'name', );
        wp_dropdown_categories( $dropdown_options );
    }
}
add_action( 'restrict_manage_posts', 'olab_add_image_category_filter' );

Make sure to replace ‘olab’ with whatever prefix you’re using in your theme and/or plugin.

Simple grids on the fly with Compass

So I made my grid system. It worked great.

Then I started building a site in Drupal.

It’s hardly a secret that Drupal’s markup is really terrible. I have to control my urge to run back screaming to WordPress and never leave again! But such is life. Drupal sure has it strong points and nowadys I actually mostly enjoy working with it. Having said that 4,000 divs doesn’t always make working with the front end so easy, especially if we’re working with a CSS grid.

So, I wrote a tiny mixin for SASS/Compass that can will work out your grids for you when the markup is against you:

$columns: 16;
$gridWidth: 960px;
@mixin drupalCols($rows: 4, $padding: 0 10px) {
  width: ($gridWidth / $columns) * $rows;
  float: left;
  padding: $padding;
  @include box-sizing(border-box);
}

.my-div {
  @include drupalCols(8);
}

// This becomes:
/*
.my-div {
  width: 480px;
  float: left;
  padding: 0 10px;
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}
*/

// You can also add more or less padding like this:

.my-div {
  @include drupalCols(4, 10px 20px 30px);
}

/*
.my-div {
  width: 240px;
  float: left;
  padding: 10px 20px 30px;
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}
*/

This one has fixed pixel values because the project I wrote it for is not responsive and is based on a grid 960px wide divided into 16 sections (yes 16)!

Here’s one that is responsive based on 12 columns:

$columns: 12;
$gridWidth: 100%;
@mixin drupalCols($rows: 4, $padding: 0 10px) {
  width: ($gridWidth / $columns) * $rows;
  float: left;
  padding: $padding;
  @include box-sizing(border-box);
}

.my-div {
  @include drupalCols(4);
}

// This becomes:
/*
.my-div {
  width: 33.333%;
  float: left;
  padding: 0 10px;
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}
*/

For the responsive one to work (if you’ve got full control of your markup) then you could use this:

@mixin clearfix {
  &:after {
    content: " ";
    display: table;
    clear: both;
  }
}

$columns: 12;
@while $columns > 0 {
  .span-#{$columns} {
    width: percentage(0.083333 * $columns);
  }
  $columns: $columns - 1;
}

$gutter: 10px;
.row-container {
  margin: $gutter 0;
  @include clearfix;
  > div {
    border-right: $gutter solid transparent;
    border-left: $gutter solid transparent;
    float: left;
    @include clearfix;
    @include box-sizing(border-box);
  }
  &:first-child {
    border-left: none;
  }
  &:last-child {
    border-right: none;
  }
}

//This produces the following:
/*
.span-12 {
  width: 100.0%;
}

.span-11 {
  width: 91.666%;
}

.span-10 {
  width: 83.333%;
}

.span-9 {
  width: 75.0%;
}

.span-8 {
  width: 66.666%;
}

.span-7 {
  width: 58.333%;
}

.span-6 {
  width: 50.0%;
}

.span-5 {
  width: 41.667%;
}

.span-4 {
  width: 33.333%;
}

.span-3 {
  width: 25.0%;
}

.span-2 {
  width: 16.667%;
}

.span-1 {
  width: 8.333%;
}

.row-container {
  margin: 10px 0;
}
.row-container:after {
  content: " ";
  display: table;
  clear: both;
}
.row-container > div {
  border-right: 10px solid transparent;
  border-left: 10px solid transparent;
  float: left;
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}
.row-container > div:after {
  content: " ";
  display: table;
  clear: both;
}
.row-container:first-child {
  border-left: none;
}
.row-container:last-child {
  border-right: none;
}
*/

// Use it like this:
/*
<div>
  <div>
    ...content...
  </div>
  ...more columns
</div>
*/

If you’re wondering about support for

box-sizing: border-box;

be happily surprised: It’s supported by all major browsers, including IE8! I tend to use it liberally these days in all my projects.