Writing PHP Tests

About the WordPress PHPUnit Test Suite

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 testunit test Code written to test a small piece of code or functionality within a larger application. Everything from themes to WordPress core have a series of unit tests. Also see regression.: 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.

Top ↑

The Basic Structure of a Test

Top ↑

Assertions

The most important part of any test is the assertion. An assertion is a comparison between the value you expect to get from the system, and the value that you actually get. The very simplest tests may consist of nothing but a single assertion. Example:

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

The assertSame() method accepts two parameters: the expected value (in this case, the hardcoded string 'foo/'), and the actual value (the value returned by trailingslashit()).

An annotated list of common assertions can be found below.

Top ↑

Fixtures and Factories

Many tests require the existence of one or more data objects, like posts or users, that WordPress normally stores in the database. Each individual test method begins with an empty WordPress installation, so each test is responsible for creating its own objects for testing. These objects are called fixtures, and they are generated by factories.

public function test_user_with_editor_role_can_edit_others_posts() {
	$user_id = self::factory()->user->create( array(
		'role' => 'editor',
	) );

	$this->assertTrue( user_can( $user_id, 'edit_others_posts' ) );
}

The create() factory method returns the numeric ID of the object. To get the object instead, use create_and_get():

public function test_user_exists() {
	$user = self::factory()->user->create_and_get();

	$this->assertTrue( $user->exists() );
}

Some tests require more than one object of a given kind. In this case, use create_many(), which uses an iterator to ensure object uniqueness (for example, unique email addresses in the case of users):

public function test_term_query_count() {
	$tags = self::factory()->term->create_many( 3, array(
		'taxonomy' => 'post_tag',
	) );

	$term_query = new WP_Term_Query();
	$actual = $term_query->query( array(
		'taxonomy' => 'post_tag',
		'fields' => 'count',
	) );

	$this->assertSame( 3, $actual );
}

Factory create*() methods accept an array of arguments, specific to that object type. For more details, see the __construct() method of each factory class at https://core.trac.wordpress.org/browser/trunk/tests/phpunit/includes/factory.

Top ↑

Shared Fixtures

When all tests within a test class need the same fixtures to be available, it is common practice to abstract the creation of these fixtures out to a “set up” method. It is also good practice, to clean up after your test and remove any fixtures you have created.

PHPUnit offers four methods which are automatically called to help with this: setUpBeforeClass(), setUp(), tearDown() and tearDownAfterClass().

In the context of the WordPress test suite, snake_case versions of these methods MUST be used. These snake_case methods provide PHPUnit cross-version compatibility.

PHPUnit method name WordPress method name
setUpBeforeClass() set_up_before_class()
setUp() set_up()
assertPreConditions() assert_pre_conditions()
assertPostConditions() assert_post_conditions()
tearDown() tear_down()
tearDownAfterClass() tear_down_after_class()

Functionality in a set_up() method will run before each test in a class, while anything in a tear_down() method will run immediately after each test in the class. In contrast, the set_up_before_class() method will run once before the class is instantiated and the tear_down_after_class() will run once after all tests in the class have been run.

By default, all test classes in the WordPress test suite extend from the WP_UnitTestCase class which inherits from the WP_UnitTestCase_Base class.

The WP_UnitTestCase_Base class already contains generic declarations of these fixture methods, which do things like: reset the database to its default state, clean up post types, taxonomies, some global variables, the registered hooksHooks In WordPress theme and development, hooks are functions that can be applied to an action or a Filter in WordPress. Actions are functions performed when a certain event occurs in WordPress. Filters allow you to modify certain functions. Arguments used to hook both filters and actions look the same. etc.

When declaring a set_up() method in a test class, it is critical that the first line of the set_up() method is a call to parent::set_up(). Similarly, tear_down() methods should always call parent::tear_down() as the very last line.

class Tests_Subset_functionName {
	public function set_up() {
		parent::set_up();
		// The specific set up for the tests within this class.
	}

	public function tear_down() {
		// Clean up specific to the tests within this class.
		parent::tear_down();
	}
}

For more information about when to use which method, please read through the Fixtures chapter in the PHPUnit manual.

Top ↑

Using Assertions

The most basic assertion available in PHPUnit is assertSame(), which performs a strict equality check === between the $expected and $actual parameters. Nearly any test can be written using this assertion.

For convenience, PHPUnit makes many more assertion methods available. It is strongly recommended to use the most specific assertion available. See the official documentation for information. As of WP 5.9, all assertions as available in PHPUnit 9.x can be used in the WordPress test suite and will work PHPUnit cross-version.

Some of the more common assertion methods:

  • assertContains() and assertNotContains()
  • assertStringContainsString() and assertStringNotContainsString()
  • assertTrue() and assertFalse()
  • assertNull(), assertEmpty(), assertNotEmpty()

In addition to assertion methods offered by PHPUnit, WordPress has a number of custom assertions method available:

  • assertEqualSets( $expected, $actual, $message = '' ) – Asserts the equivalence of two arrays, ignoring order and strict equality (eg assertEqualSets( [ 1, 2 ], [ '2', '1' ] ))
  • assertEqualSetsWithIndex( $expected, $actual, $message = '' )
  • assertSameSets( $expected, $actual, $message = '' )
  • assertSameSetsWithIndex( $expected, $actual, $message = '' )
  • assertWPError( $actual, $message = '' ) and assertNotWPError( $actual, $message = '' )
  • assertIXRError( $actual, $message = '' ) and assertNotIXRError( $actual, $message = '' )
  • assertEqualFields( $object, $fields, $message = '' )
  • assertDiscardWhitespace( $expected, $actual, $message = '' )
  • assertSameIgnoreEOL( $expected, $actual, $message = '' )
  • assertNonEmptyMultidimensionalArray( $array, $message = '' )

See a complete list in the WP_UnitTestCase_Base class.

All PHPUnit assertions, as well as all WordPress custom assertions, allow for a $message parameter to be passed. This message will be displayed when the assertion fails and can help immensely when debugging a test. This parameter should always be used if more than one assertion is used in a test method.

class Tests_Foo {
	public function test_function_call() {
		$actual = function_call();

		$this->assertIsArray( $actual, 'Return value of function_call() is not an array' );
		$this->assertArrayHasKey( 'desired_key', $actual, 'Key "desired_key" does not exist in array returned by function_call()' );
		$this->assertSame( 'expected_value', $actual['desired_key'], 'Value for array key "desired_key" does not match expectations' );
	}
}

Top ↑

Annotations

PHPUnit annotations are pieces of metadata, indicated by a @, placed in the docblockdocblock (phpdoc, xref, inline docs) of a test method or class. WordPress’s tests use a number of PHPUnit annotations, as well as a few custom ones, to organize its tests. Note that annotations belonging to a class are automatically inherited by all member test methods, and should not be appended with a full stop.

Some of the more commonly used annotations:

  • @group – Used to sort tests and test classes by functionality, into groups that can be run separately (eg $ phpunit --group comment). All test classes should have at least one @group annotation, and individual tests should have additional @group annotations as necessary.
  • @covers – Used to annotate which function, method or class is actually being tested by a particular test. It is strongly recommended to add a @covers tagtag A directory in Subversion. WordPress uses tags to store a single snapshot of a version (3.6, 3.6.1, etc.), the common convention of tags in version control systems. (Not to be confused with post tags.) for every test.
  • @requires – This annotation is used to indicate that a test has a dependency on, for instance, a specific PHPPHP The web scripting language in which WordPress is primarily architected. WordPress requires PHP 5.6.20 or higher (minimum) version, a PHP extension or a particular function being available. If a test can only succeed if such a dependency exists and is fulfilled, the @requires tag should be added to the test docblock. The PHPUnit manual has some great examples of how to use this tag.
  • @ticket – A custom WordPress annotation. Use @ticket 12345 to indicate that a test addresses the bugbug A bug is an error or unexpected result. Performance improvements, code optimization, and are considered enhancements, not defects. After feature freeze, only bugs are dealt with, with regressions (adverse changes from the previous version) being the highest priority. described in ticketticket Created for both bug reports and feature development on the bug tracker. #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.

Top ↑

Naming and Organization

The WordPress team strives to make tests easy to find, easy to maintain, and easy to run in isolation. As such, we have a number of guidelines for organizing and naming tests.

All PHPUnit tests are located in the /tests/phpunit/tests/ directory. Paths given below are relative to this root.

Top ↑

Test Classes

Tests are organized into classes. Group tests into a class when they test different aspects of the same piece of functionality. It’s especially convenient to put tests together in a class when they can share a common set_up() or set_up_before_class() routine. As a rule, a single test class should not contain tests for more than one function/method.

Each test class should be in its own file. Test files are sorted into folder based on shared functionality, which generally matches the primary @group annotation for those tests. Test files should be named using camelCase.

Test class names should reflect the filepath, with underscores replacing directory separators and TitleCase replacing camelCase. Thus, the class Tests_Comment_GetCommentClass(), which contains tests for the get_comment_class() function, is located in tests/comment/getCommentClass.php.

Top ↑

Test Methods

Method names in a test class must begin with test_. Methods that do not begin with test_ will not be run by PHPUnit; filterFilter Filters are one of the two types of Hooks https://codex.wordpress.org/Plugin_API/Hooks. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output. callbacks or other utility methods should not have this prefix. Test method names should be written in snake_case.

The test name should be as descriptive as possible. For example, Tests_Comment_GetCommentClass::test_should_accept_comment_id() tests whether get_comment_class() accepts a comment ID. Test names that describe the desired outcome of the test, ideally containing the word ‘should’, make debugging far easier; a good test name might make it unnecessary to look at the test source when a failure is spotted during a test run.

Top ↑

Advanced Topics

Top ↑

Slow Fixtures

Sometimes, all tests in a class can use the same database fixtures. Generating these fixtures inside of set_up() will cause them to be recreated for each test, which can dramatically decrease test performance. In these cases, it’s possible to create fixtures once, before any tests in the class have run, using wpSetUpBeforeClass():

class Tests_Comment_Stuff {
	protected static $comment_post_id;

	public static function wpSetUpBeforeClass( $factory ) {
		self::$comment_post_id = $factory->post->create();
	}
}

Similarly, wpTearDownAfterClass() can be used for cleanup after all of a class’ tests have been run (though note that this is generally unnecessary, as the test suite tries to clean up all shared fixtures automatically).

Some notes about shared fixtures:

  • wpSetUpBeforeClass() and wpTearDownAfterClass() must be declared as static and called statically.
  • Because the methods are static, the way that the data is stored for use in the test methods must also be static: self::$foo rather than $this->foo.
  • Do not call self::factory() in wpSetUpBeforeClass(), as it will break the uniqueness iterator; always use the $factory object passed to the method.

Top ↑

Repetitive Tests

Whenever you have the inclination to copy and paste the code of a test method to test the same WordPress functionality with only slightly different arguments, please consider using a data provider instead.

A data provider is a secondary function which is linked to a test using a @dataProvider annotation in the docblock of the test method and is generally named after the test, replacing the test prefix in the method name with data.

This second function must return an array of arrays, where each entry in this array is called a data set and passed to the test function as input parameters.

It is highly recommended to use named data sets in a data provider.

Top ↑

One-off Functions for Hooks

If your test needs a one-off callback function to hook into an action/filter, please feel free to use a closure (anonymous function).

And if your closure does not use the $this variable, please declare the closure as static for optimal performance.

Using closures as hook callbacks in CoreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress./plugins/themes is not recommended, as anonymous functions do not allow for unhooking the callback from the hook. However, this is not a problem in the test suite, as the whole stack of registered actions and filters will be reset after each test via the shared tear_down() method.

Top ↑

Technical Overview

For the curious, here’s a quick overview of how PHPUnit is leveraged to work with an application like WordPress.

Top ↑

The WordPress Installation and Bootstrap

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’ categoryCategory The 'category' taxonomy lets you group posts / content together that share a common bond. Categories are pre-defined and broad ranging..

Top ↑

Globals

The WP globals that reflect current page state, such as $wp and $wp_query, are reset to a default state after each test is run. Similarly, post-related globals like $post and $more are set to null after each test. The test suite also resets runtime-registered object types to their defaults; this means that custom post types, taxonomies, etc must be reregistered for every test. Last but not least, all actions and filters are reset to their original state after each test as well, so it is not necessary to manually remove hooks that have been added in a test.

Modifications to other globals should be managed carefully by the tests themselves. A simple convention to do so is to store the original global in a local variable and reset it back to that value after the test logic has been run. It is important that such resets should happen before the actual assertion is made, so that subsequent test results are not polluted by one possible failure.

Top ↑

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 MySQLMySQL MySQL is a relational database management system. A database is a structured collection of data where content, configuration and other options are stored. https://www.mysql.com/. 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 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.

Last updated: