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; they should be taken as a conceptual reference.
WP PHPUnit is a framework that is used as a utility tool to write test cases related to the 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.
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:
The following are reset to their original 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.
class Simple_TestCase extends WP_UnitTestCase {
}
WP_UnitTestCase is an abstract class that inherits all the testing utility methods from the WP_PHPUnit Framework and PHPUnit Framework.
We need our TestCass to extend class WP_UnitTestCase in order to utilize those methods using the $this keyword.
These annotations are exclusive to WP_PHPUnit:
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 completely new database, it is very convenient to create posts using factory classes.
The factory has the following properties that you can use:
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.
In this lesson, we’ll go through the basics of testing custom post types. We need to test the following:
There are a lot of possibilities for test cases, but it is important to keep the test cases to a minimum and to the point.
As a best practice, it is better to include test cases that have important business logic, features, behavior, etc., so that their functionality or behavior will not be altered unintentionally during 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
);
}
}
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'));
}
}
Let’s test a basic shortcode in this example:
Things to test:
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, and 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 takes parameters, we can test it using expected results by passing the arguments.
Let’s add a custom REST endpoint that 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:
<?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:
Writing test cases for custom classes depends on the functionality. If there is any important functionality or logic, we must prioritize the creation of test cases.
Test case to check the expected results of custom_logic():
class My_Custom_Class {
public function custom_logic( $a, $b ) {
return $a + $b;
}
}
<?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() );
}
}