Four WordPress integration testing easy pieces

Know the very basics of WordPress integration testing.

The testing context

To clear the confusion that might arise from different testing jargon and habits I’m calling “WordPress integration testing” any test that is based on the Core PHPUnit-based automated testing suite; in this bucket fall the Core suite itself as well as the integration tests scaffolded using wp-browser wpunit generation command:

wpcept generate:wpunit integration Example

The typical test case will have a starting code like this if scaffolded with wp-browser:

<?php

class ExampleTest extends \Codeception\TestCase\WPTestCase
{
    public function setUp()
    {
        // before
        parent::setUp();

        // your set up methods here
    }

    public function tearDown()
    {
        // your tear down methods here

        // then
        parent::tearDown();
    }
}

and like this if built on top of the Core suite:

class ExampleTest extends WP_UnitTestCase
{
    public function setUp()
    {
        // before
        parent::setUp();

        // your set up methods here
    }

    public function tearDown()
    {
        // your tear down methods here

        // then
        parent::tearDown();
    }
}

For the purpose of this test the two bear no significant differences.

There is no database

Test methods will interact with the database in a transaction that will be rolled back when the tests ends.
This means there will be no real data written on the database during the tests and why running the tests with XDebug hoping to capture a “snapshot” of the database during the tests is a failing proposition.
This is common practice in integration testing and the answer to the need to “take a look at the database” is to write more assertions.

Factories

One of the strongest point of the suite are its factories.
Factories allow testers to setup complex fixtures with relatively little code and explain the lack of a dump importing option in the WPLoader module or the Core suite.
Before any test method runs, the suite will install WordPress in single or multisite mode, empty the database of any data to start the test from a blank slate.
This means the site will be empty like after a fresh install and I’ve seen instances where developers came up with byzantine solutions to set up the test pre-conditions.
I’ve rarely had the need to add to the powerful factories code and anyone willing to test should knowledge of their existence.
Test case factories are accesssed using the factory() method:

public function test_post_factory()
{
    $this->factory()->post->create();

    $this->assertCount(1, get_posts());

    $this->factory()->post->create_many(4);

    $this->assertCount(5, get_posts());

    $post = $this->factory()->post->create_and_get();

    $this->assertInstanceOf('WP_Post', $post);
}

public function test_post_factory_can_override_defaults()
{
    $post = $this->factory()->post->create_and_get(['post_title' => 'Hello there']);

    $this->assertEquals('Hello there', $post->post_title);
}

On the same note factories exist for users, taxonomy terms and so on.

public function test_factories()
{
    $this->assertNotEmpty($this->factory()->post);
    $this->assertNotEmpty($this->factory()->category);
    $this->assertNotEmpty($this->factory()->tag);
    $this->assertNotEmpty($this->factory()->term);
    $this->assertNotEmpty($this->factory()->attachment);
    $this->assertNotEmpty($this->factory()->comment);
    $this->assertNotEmpty($this->factory()->user);
    if (is_multisite()) {
        $this->assertNotEmpty($this->factory()->network);
        $this->assertNotEmpty($this->factory()->blog);
    }
}

Integration factory testing

Note that network and blog factories will not be available if not running tests in multisite mode.
If, on the site project scale, “there is probably a plugin that does that”, on the integration tests scale “there is probably a factory that does that”.
As it’s the case for so much that is at the intersection of WordPress and testing the documentation is scarce, where any, and the best place to find out more is the code itself.

It’s WordPress for real

In the context of an integration test any WordPress function, class and method will be available.
WordPress has been loaded before the tests in the same scope as the test case itself hence the test case methods, factories being the most relevant but not the only ones, are available along with all of WordPress functions.

public function test_trailingslashit()
{
    $this->assertEquals('foo/', trailingslashit('foo'));
}

Integration WordPress methods testing

WordPress global context too will be available:

public function test_wpdb()
{
    $this->factory()->post->create_many(5, ['post_status' => 'draft']);

    /** @var \wpdb $wpdb */
    global $wpdb;

    $query = "SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_status = 'draft'";
    $this->assertEquals(5, $wpdb->get_var($query));
}

Integration WordPress global context testing

Current user

That blank slate covers not only posts but users and blogs (in the case of multisite) too.
What is the current user in a test method? A test is the best way to find out:

public function test_current_user_id() {
    $this->assertEquals(0, get_current_user_id());
}

public function test_current_user_id_a_second_time() {
    $this->assertEquals(0, get_current_user_id());
}

Integration user testing

Both tests will succeed. Keeping this information in mind is important to understand why the similar tests below will have different results:

public function test_adding_a_post_term($value = '')
{
    register_taxonomy('my-tax', 'post');
    wp_insert_term('term1', 'my-tax');

    $ids = $post = $this->factory()->post->create_many(5);

    foreach ($ids as $id) {
        wp_set_post_terms($id, ['term1'], 'my-tax');
    }

    $this->assertCount(5, get_posts([
        'post_type' => 'post',
        'tax_query' => [
            [
                'taxonomy' => 'my-tax',
                'field' => 'slug',
                'terms' => 'term1'
            ]
        ]
    ]));

}

public function test_adding_a_post_term_directly($value = '')
{
    register_taxonomy('my-tax', 'post');
    wp_insert_term('term1', 'my-tax');

    $post = $this->factory()->post->create_many(5, ['tax_input' => ['my-tax' => 'term1']]);

    $this->assertCount(5, get_posts([
        'post_type' => 'post',
        'tax_query' => [
            [
                'taxonomy' => 'my-tax',
                'field' => 'slug',
                'terms' => 'term1'
            ]
        ]
    ]));
}

Failing terms post testing

The test methods will set up the same fixture of 5 posts assigning each a term from the my-tax taxonomy but the second one does it leveraging the factory use of, under the hood, the wp_inser_post function; when processing the tax_input entry the wp_insert_post function will run this check:

if ( current_user_can( $taxonomy_obj->cap->assign_terms ) ) {
    wp_set_post_terms( $post_ID, $tags, $taxonomy );
}

while calling wp_set_post_terms() directly will skip the check.
The reason of the second test failure is that the current user is 0: the non authenticated visitor that cannot, of course, add terms.
Keeping this in mind the second test can be updated to:

public function test_adding_a_post_term_directly($value = '')
{
    wp_set_current_user(1);

    register_taxonomy('my-tax', 'post');
    wp_insert_term('term1', 'my-tax');

    $post = $this->factory()->post->create_many(5, ['tax_input' => ['my-tax' => 'term1']]);

    $this->assertCount(5, get_posts([
        'post_type' => 'post',
        'tax_query' => [
            [
                'taxonomy' => 'my-tax',
                'field' => 'slug',
                'terms' => 'term1'
            ]
        ]
    ]));
}

User 1 is always an administrator (or the super-administrator in multisite) but the even better way to do it, to decouple the test code from this assumption that might change in the future, is to create a new Administrator completely:

public function test_adding_a_post_term_directly($value = '')
{
    $user = $this->factory()->user->create(['role' => 'administrator']);
    wp_set_current_user($user);

    register_taxonomy('my-tax', 'post');
    wp_insert_term('term1', 'my-tax');

    $post = $this->factory()->post->create_many(5, ['tax_input' => ['my-tax' => 'term1']]);

    $this->assertCount(5, get_posts([
        'post_type' => 'post',
        'tax_query' => [
            [
                'taxonomy' => 'my-tax',
                'field' => 'slug',
                'terms' => 'term1'
            ]
        ]
    ]));
}

Integration passing post term testing

Next

The examples above are respectable pieces to digest and I will cover multisite and some more advanced operations next.