Chapter 17 — Front-End Fundamentals

WordPress Unit Testing

The plugin from Chapter 16 works. You activated it, the portfolio post type appeared, the archive resolved. But “works” is a snapshot. Six months from now you’ll change something, and the question is whether you’ll know if that change broke the post type registration. Tests answer that question. This chapter sets up a test suite for the plugin and writes the first tests against it.

Why this is harder than testing plain PHP

A unit test for a plain PHP function is straightforward: call the function with known input, assert the output is what you expect, done. The function depends on nothing but its arguments.

WordPress code is not like that. WordPress code calls get_post(), runs WP_Query, reads options from the database, checks the current user, fires hooks. None of that works without a database connection, a loaded WordPress environment, and a pile of global state already in place. Call get_post() from a bare PHPUnit test and PHP throws a fatal error, because the function isn’t defined. There’s no WordPress for it to be part of.

The WordPress core team solved this with a test suite: a minimal WordPress installation, built to be loaded by tests. Your tests boot that installation first, then run against it, so by the time your test calls post_type_exists() or creates a post, WordPress is genuinely there to respond. This isn’t accidental complexity. It’s the unavoidable shape of testing code whose entire job is to call WordPress functions.

PHPUnit

PHPUnit is the standard testing framework for PHP. It’s what nearly every PHP project uses, WordPress included. It provides the test runner, the assert methods, and the test case base class everything else builds on.

Install it as a development dependency with Composer, the PHP package manager. From the plugin directory:

bashwp-content/plugins/tutorials-plugin
composer require --dev phpunit/phpunit

--dev marks PHPUnit as a development-only dependency. It’s needed to run tests and isn’t shipped with the plugin to production. Composer writes it to composer.json under require-dev and installs the PHPUnit binary into vendor/bin/.

PHPUnit can also be installed globally, by downloading the .phar file and putting it on your PATH. Either approach works. Per-project installation with Composer is the modern default, because it pins an exact PHPUnit version to the project, and that’s what this chapter uses.

Scaffolding the test setup with WP-CLI

Setting up the WordPress test suite by hand means downloading the right files, writing a phpunit.xml, and wiring up a bootstrap that loads WordPress before the tests. WP-CLI, the command-line interface for WordPress, does all of it for you with one command.

From the plugin directory:

bashwp-content/plugins/tutorials-plugin
wp scaffold plugin-tests tutorials-plugin

This generates three things.

tests/ is the directory your test files go in. It comes with a bootstrap.php that loads the WordPress test environment and your plugin, plus an example test file you’ll replace.

phpunit.xml.dist is the PHPUnit configuration. It tells PHPUnit where the tests are, which bootstrap to run first, and how to report results. The .dist suffix marks it as the distributed default; you can copy it to phpunit.xml and customize locally without touching the shared version.

bin/install-wp-tests.sh is a shell script that downloads the WordPress test suite and creates the test database. You run it once, before the first test run.

The install script

bin/install-wp-tests.sh does the part that makes WordPress testing possible: it downloads a copy of WordPress built for testing and sets up a separate database for tests to use. Separate is the key word. Tests create and delete posts constantly, and you do not want that happening against the database holding your real content. The test database is disposable by design.

Run it once:

bashwp-content/plugins/tutorials-plugin
bash bin/install-wp-tests.sh tutorials_test root '' localhost latest

The five arguments, in order: the name for the test database (tutorials_test), the database user (root), the database password (empty string here, quoted as ''), the database host (localhost), and the WordPress version to test against (latest). Adjust the user, password, and host to match your local database setup.

After this runs, the test database exists and the WordPress test files are downloaded. You won’t run this command again unless you want to refresh the test environment.

What a WordPress unit test looks like

A WordPress test is a class that extends WP_UnitTestCase. That base class is WordPress’s extension of PHPUnit’s TestCase. It adds two things you’ll use constantly: a clean WordPress environment for every test, and a set of factories for creating test data.

Replace the scaffolded example test with a real one for the portfolio post type:

php~/tutorials/wp-content/plugins/tutorials-plugin/tests/test-cpt.php
<?php
class Test_Tutorials_CPT extends WP_UnitTestCase {

	public function test_portfolio_post_type_registered() {
		$this->assertTrue(
			post_type_exists( 'portfolio' ),
			'The portfolio custom post type should be registered.'
		);
	}

	public function test_portfolio_post_can_be_created() {
		$post_id = $this->factory->post->create( array(
			'post_type'   => 'portfolio',
			'post_title'  => 'Test Portfolio Item',
			'post_status' => 'publish',
		) );

		$this->assertIsInt( $post_id );

		$post = get_post( $post_id );
		$this->assertEquals( 'portfolio', $post->post_type );
	}
}

A few things to understand here.

WP_UnitTestCase resets WordPress between tests. Each test method starts against a clean environment, and any database changes a test makes are rolled back when it finishes. Tests don’t leak state into each other, and they don’t leave anything behind in the test database.

$this->factory is the factory system. $this->factory->post->create() inserts a real post into the test database and returns its ID. The factory has builders for posts, users, terms, comments, and more. These are real database records, not mocks, which is why the rollback matters: the post created in test_portfolio_post_can_be_created exists for the duration of that test and is gone after it.

The methods starting with assert are PHPUnit assertions, the actual checks. assertTrue() fails the test unless its argument is true. assertIsInt() fails unless its argument is an integer. assertEquals() fails unless its two arguments are equal. The optional last argument to an assertion is a message printed when it fails, and it’s worth writing one, because a clear failure message turns a red test into a precise description of what went wrong.

The first test confirms the post type is registered. The second confirms a portfolio post can actually be created and comes back with the right type. Together they cover the core promise of the plugin.

What to test, and what to skip

Test your own code. Skip WordPress core.

The plugin’s job is to register a post type. So the tests check that the post type registers and that posts of that type behave correctly. If you add a content filter, test that the filter transforms content the way you intended. If you add a validation function, test that it returns the right value for valid input and the right value for invalid input. The rule: test the behavior you wrote.

Don’t test WordPress itself. When test_portfolio_post_can_be_created calls get_post() and gets a post object back, that’s WordPress doing its job. get_post() working correctly is WordPress’s responsibility to test, and the core team tests it thoroughly. Writing a test that amounts to “does get_post() return a post” tests nothing you own and adds maintenance for no coverage. The same goes for any third-party plugin’s code: not yours, not your test’s concern.

The line is ownership. Your code, your test. Someone else’s code, someone else’s test.

Running tests

With PHPUnit installed and the test database in place, run the suite from the plugin directory:

bashwp-content/plugins/tutorials-plugin
./vendor/bin/phpunit

If you installed PHPUnit globally instead, the command is just phpunit. Either way, PHPUnit reads phpunit.xml.dist, runs the bootstrap that loads WordPress, executes every test method, and prints a summary: how many tests ran, how many assertions, how many passed, and the details of anything that failed. A green result means every test passed. A red one points you straight at the method and the assertion that broke.

Integration tests versus unit tests

One honest clarification on terminology. A unit test, in the strict sense, isolates a single function from all its dependencies and tests it alone. By that definition, what you wrote in this chapter is not a unit test. It boots a real WordPress, talks to a real database, and creates real posts. That’s an integration test: it tests your code integrated with the system it runs inside.

WP_UnitTestCase has “unit” in its name, and the WordPress community calls these tests unit tests by convention, but they are integration tests by the strict definition. That’s fine. For WordPress code, integration tests are usually the more useful kind anyway, because so much of what your code does only has meaning in the context of a running WordPress. Testing tutorials_register_portfolio_cpt() in true isolation, with WordPress mocked out, would tell you very little. Testing it against a real WordPress tells you whether the post type actually registers.

Don’t get stuck on the label. A test that catches a regression before it reaches a client’s site has done its job, whatever you call it.

Commit your work

You’ve added a PHPUnit test suite to the plugin: the scaffold, the test database, and the first tests against the portfolio post type. Commit it.

References