Writing PHPUnit Tests

About the WordPress PHPUnit Test Suite 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 test: a test that verifies a piece of functionality in complete isolation, without any external dependent 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 The Basic Structure of a Test

Assertions 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 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 ↑

Assertions 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. See the official documentation for information. (As of this writing, WP’s suite supports PHPUnit 5.7; avoid using assertion methods introduced after this.) Some of the more common assertion methods:

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

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

  • assertEqualSets() – Asserts the equivalence of two arrays, ignoring order and strict equality (eg assertEqualSets( [ 1, 2 ], [ '2', '1' ] ))
  • assertWPError() and assertNotWPError()

See a complete list in tests/phpunit/includes/testcase.php.

Top ↑

Annotations Annotations

PHPUnit annotations are pieces of metadata, indicated by a @, placed in the docblock 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. 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.
  • @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.

Top ↑

Naming and Organization 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 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 setUp() or setUpBeforeClass() 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 Test Methods

Methods in a test class must begin with test_. Methods that do not begin with test_ will not be run by PHPUnit; filter callbacks or other utility methods should not have this prefix. Test methods 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 Advanced Topics

Top ↑

Related tests are generally grouped together in classes. Sometimes, each test method in a class requires exactly the same setup steps. Functionality in a setUp() method will run before each test in a class, while anything in a tearDown() method will run immediately after each test in the class. In the following example, all test methods will run in an environment where posted comments will skip the approval process:

class Tests_Comment_Assume_Approved {
    public function setUp() {
        parent::setUp();
	add_filter( 'pre_comment_approved', '__return_true' );
    }

    public function tearDown() {
	remove_filter( 'pre_comment_approved', '__return_true' );
        parent::tearDown();
    }
}

When using defining setUp() in a test class, it is critical that the first line of the setUp() method call parent::setUp(). Similarly, tearDown() methods should always call parent::tearDown() as the very last line.

Top ↑

Shared Fixtures Shared Fixtures

Sometimes, all tests in a class can use the same database fixtures. Generating these fixtures inside of setUp() 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’s 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 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 ↑

Technical Overview 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 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’ category.

Top ↑

Globals 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 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 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.