Writing Unit Tests for WordPress Plugins & Themes

In this lesson we’ll go through some fundamental test cases using commonly used features in Plugins and themes. 

Note: All the test cases use minimal code for simplicity, it should be taken as a conceptual reference.

Introduction to WP PHPUnit

WP PHPUnit is a framework which is used as a utility tool write test cases related WordPress environment.

The WordPress PHPUnit Test Suite contains thousands of automated tests. Automated tests are small bits of code that verify a specific piece of WP functionality. These tests are a powerful tool both for feature development and for the prevention of regressions.

  • Automated tests should be as small and as specific as possible.
  • Ideally, an automated test will be a unit test:
    • a test that verifies a piece of functionality in complete isolation, without any dependency on the overall state of the system.
  • In practice, the structure of WordPress and its test suite makes it difficult or impossible to write “pure” unit tests.
  • Here and elsewhere, we use the term “unit test” to refer loosely to any automated test in the suite.

Bootstrap Process of WP_PHPUnit Tests

When phpunit is invoked, the test suite runs a script that sets up a default installation of WordPress, with a configuration similar to what you get with the GUI install. Before any tests are run, the following steps take place:

  • WordPress is bootstrapped (by including wp-settings.php). This means that all tests run after the entire WP bootstrap (through wp_loaded).
  • All default content is deleted. This includes sample posts and pages, but does not include the default user or the ‘Uncategorized’ category.

The Following are reset to original state

  • global $wp
  • global $wp_query
  • $post is to null
  • PostTypes & Taxonomies are registered
  • Filters are reset to initial state

Test Database

WordPress keeps both data (posts, users) and state (options) in the database. And the structure of WordPress is such that it’s almost impossible to mock fixtures and settings without actually using a database. As such, the test suite does use a MySQL database for setting up the WP application and for storing fixtures and other data.

It’s important to distinguish between persistent and non-persistent database content in the test suite. When phpunit is invoked, the test suite wipes the test database clean and performs a clean installation. This data – such as the default content of wp_options – is persistent through the tests.

Database modifications made during the test, on the other hand, are not persistent. Before each test, the suite opens a MySQL transaction (START TRANSACTION) with autocommit disabled, and at the end of each test the transaction is rolled back (ROLLBACK). This means that database operations performed from within a test, such as the creation of test fixtures, are discarded after each test. For more information on transactions, see the official MySQL documentation, and especially the section on statements that trigger commits.

The class WP_UnitTestCase

class Simple_TestCase extends WP_UnitTestCase {

}

 WP_UnitTestCase  is an abstract class which inherits all the testing utility methods from WP_PHPUnit Framework and PHPUnit Framework.

We need to extend our TestCass class inorder to utilize those methods using $this keyword.

Annotations in WP_PHPUnit

These annotations as exclusive to WP_PHPUhit

  • @ticket – A custom WordPress annotation. Use @ticket 12345 to indicate that a test addresses the bug described in ticket #12345. Internally, @ticket annotations are translated to @group, so that you can limit test runs to those associated with a specific ticket: $ phpunit –group 12345.
  • @expectedDeprecated – Custom to WordPress. Indicates that a _deprecated_*() notice is expected to be thrown by the specified function/method/class. Without this annotation, tests that trigger a deprecation notice will fail; similarly, if you include this annotatation but the test does not trigger a deprecation notice, the test will fail. For example, tests for the deprecated like_escape() contain the annotation @expectedDeprecated like_escape.
  • @expectedIncorrectUsage – Similar to @expectedDeprecated, but for _doing_it_wrong() notices.

Factory Classes

PHP Factory classes are provided by WP PHPUnit. It is used as a Utility class to create posts, users, terms, categories etc in the test environment while running testing.

As every test run deletes the database and uses a complete new database, it is very convenient to create posts using Factory classes.

The factory has the following properties that you can use:

  • $post
  • $attachment
  • $comment
  • $user
  • $term
  • $category
  • $tag
  • $blog

Examples for creating users:

$user_id = $this->factory->user->create();

Example for creating posts:

$post_id = $this->factory->post->create( array( ‘post_author’ => $user_id ) );

They may all be used in the same manner as demonstrated in the above example with the $user / $post factory.

You can learn more about factory classes here:

Note: This is the source code for factory classes, the code is readable and self-explanatory.

Testing Custom Post Types

In this lesson, we’ll go through the basics of testing Custom Post Types. We need to test the following:

  • Whether the post type exists.
  • Important labels / parameter
    • rest_base is just an example, but you are free to test any parameter which has high priority
    • rewrite rules of the URLs

There are lot of possibilities of tests cases, but it is important keep the test cases to a minimum and to the point.

As a best practice, it is better to include test cases which have an important business logic, feature, behavior etc so that it’s functionality or behavior will not be altered unintentionally while the development as the tests will fail if it happens.

<?php

namespace RTC\\Tests\\Plugin;

use WP_UnitTestCase;

class Test_All_Post_Types extends WP_UnitTestCase {
public function test_rtc_movie() {
$post_type = get_post_type_object( ‘cpt’ );

$this->assertTrue( post_type_exists( ‘cpt’ ) );

$this->assertSame( ‘CPT’, $post_type->labels->name );
$this->assertSame( ‘cpt’, $post_type->rest_base );
$this->assertSame( true, $post_type->public );
$this->assertSame( array(
‘slug’       => ‘cpt/%taxonomy%/%postname%-%post_id%’,
‘with_front’ => false,
),
$post_type->rewrite
);
}
}

Testing Custom Taxonomies

This is similar to testing custom post types:

<?php

namespace RTC\Tests\Plugin;

use WP_UnitTestCase;

class Test_All_Taxonomies extends WP_UnitTestCase {
public function test_rtc_movie() {
$taxonomy = get_taxonomy( ‘taxonomy’ );
$this->assertTrue(taxonomy_exists(‘taxonomy’));
}
}

Testing Shortcodes

Let’s test a basic shortcode in this example:

Things to test:

  • Is the shortcode getting registered?
  • Is the markup correct?
function register_shortcode() {
add_shortcode( ‘test_shortcode’, ‘example_shortcode’ );
}

add_action(‘init’, ‘register_shortcode’);

function example_shortcode($atts, $content) {
return ‘Works’;
}

In the first test case we are trying to test if the shortcode exists.

In the second test case we are checking the markup of the shortcode

<?php

namespace RTC\\Tests\\Plugin;

use WP_UnitTestCase;

class Test_Shortcodes extends WP_UnitTestCase {
public function setUp(): void {
parent::setUp();
}

public function test_shortcode() {
$this->assertTrue( shortcode_exists( ‘test_shortcode’ ) );
}

public function test_markup() {
$markup = do_shortcode( ‘[test_shortcode]’ );
$this->assertSame( ‘Works’, $markup );
}
}

Advanced testing:

If the shortcode take parameters, we can test it using expected results passing the arguments.

Testing Rest Endpoints

Let’s a add a custom rest endpoint which sends a string as a response.

<?php

add_action(
‘rest_api_init’,
‘custom_rest_route’
);

function custom_rest_route() {
register_rest_route(
‘foo’,
‘/foo’,
array(
‘methods’  => ‘GET’,
‘callback’ => ‘custom_rest_response’,
)
);
}

function custom_rest_response() {
return ‘test’;
}

In this test case we are adding a fixture $this->server. We need this in order to replicate the wp_rest_server global variable.

We initialize REST server using do_action( ‘rest_api_init’ );

You can send a REST request using $response = $this->server->dispatch( $request );

Things to test:

  • Check if the route is available
  • Check the response of the route
<?php

namespace RTC\\Tests\\Plugin;

use WP_REST_Request;
use WP_UnitTestCase;

/**
* @property \\WP_REST_Server $server
*/
class Test_Rest_Endpoints extends WP_UnitTestCase {
/** @var WP_REST_Server $wp_rest_server */
private $server;

public function setUp(): void {
global $wp_rest_server;
$this->server = $wp_rest_server = new \\WP_REST_Server();
do_action( ‘rest_api_init’ );
parent::setUp(); // TODO: Change the autogenerated stub
}

public function test_route_registration() {
$namespaces = $this->server->get_namespaces();
$this->assertTrue( in_array( ‘foo’, $namespaces, true ) );

$custom_routes = $this->server->get_routes( ‘foo’ );
$this->assertArrayHasKey( ‘/foo/foo’, $custom_routes );
}

public function test_response() {
$request  = new WP_REST_Request( ‘GET’, ‘/foo/foo’ );
$response = $this->server->dispatch( $request );
$expected = ‘test’;

$this->assertEquals( $expected, $response->data );
}
}

More test cases:

  • If the endpoint accepts arguments, test if it’s returning expected output by passing expected arguments.
  • Test if the permission callback is working as expected
  • Test the schema of the returned data.

Testing Custom Classes

Writing test cases for custom classes depends on the functionality.

If there is an important functionality or logic, we need to write test cases for them as a priority.

class My_Custom_Class {
public function custom_logic( $a, $b ) {
return $a + $b;
}
}

Test case to check the expected results of custom_logic()

Testing Theme info

You can also check important theme info details like version, Theme URI etc.

You can switch to current theme using switch_theme()

Get the WP_Theme object by using wp_get_theme();

Using the object’s getter function you can get theme details.

<?php

namespace RTC\\Tests\\Plugin;

use WP_UnitTestCase;

class Test_Theme_Info extends WP_UnitTestCase {

public function test_theme_info() {
switch_theme( ‘twentytwentyone-child’ );
$theme_info = wp_get_theme();
$this->assertSame( ‘1.0.0’, $theme_info->get( ‘Version’ ) );
$this->assertSame( ‘rtCamp’, $theme_info->get( ‘Author’ ) );
$this->assertSame( ‘twentytwentyone-child’, $theme_info->get_stylesheet() );
}
}