wp scaffold plugin-tests generates a working PHPUnit setup for a WordPress plugin in one command, and then the install script falls over on MySQL credentials. This covers the PHPUnit basics, what the scaffold writes, the three lines that need fixing, and the naming rule that quietly decides whether your tests run at all.
Two hyphens, not one dash: wptexturize rewrites commands copied off a page.
What a test buys you
You state the expected outcome once, in code, instead of clicking the same path by hand after every change. That is the entire pitch and it is enough. ✅
A test with no WordPress in it
Extend PHPUnitFrameworkTestCase:
<?php
use PHPUnitFrameworkTestCase;
class MovieLibraryTest extends TestCase {
public function test_totals_add_up() {
$expected = 5;
$result = $this->get_total( 3, 2 );
$this->assertSame( $expected, $result );
}
public function get_total( $a, $b ) {
return $a + $b;
}
}Run everything in the directory:
./vendor/bin/phpunit testsassertSame compares expected against actual and decides the outcome. Most of testing is that line, repeated. ⚖️
The two naming rules
- a file is a test file if the name ends in
Test.php->MovieLibraryTest.php - a method is a test if the name starts with
test, or if it carries a/** @test */annotation
Both method forms work:
/** @test */
public function totals_add_up() {}
public function test_totals_add_up() {}Pick one and stay with it. A method called check_totals with no annotation is not a failing test, it is a test that was never there, and the run still comes back green. 🙃
Plain PHPUnit
- Discovery by suffix Test.php
- MovieLibraryTest.php
- PHPUnit default, no config needed
Scaffolded plugin
- Discovery by prefix test-
- test-movie-library.php
- Set in phpunit.xml.dist testsuite
Same tool, opposite convention. The only symptom of getting it wrong is zero tests collected.
Scaffolding the WordPress version
From inside the plugin directory, which needs a valid plugin header before any of this works:
wp scaffold plugin-tests <plugin-name>That writes more than the name suggests:
bin/install-wp-tests.sh-> sets up the test suite and the test databasetests/bootstrap.php-> activates the plugin for the test runtests/test-sample.php-> one working testphpunit.xml.dist-> the PHPUnit config.phpcs.xml.dist-> a PHP_CodeSniffer ruleset, thrown in free. Running it is a separate job.circleci/config.yml-> CI config, and--cichanges which provider you get
Then build the test database:
./bin/install-wp-tests.sh <db-name> <db-user> <db-pass> [db-host] [wp-version]db-host and wp-version are optional. The database you name gets dropped and recreated, so do not point it at one you want to keep. 🗑️
The MySQL flags the script assumes
If your local MySQL wants credentials, three calls in install-wp-tests.sh need them passed explicitly. In recreate_db:
mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRAIn create_db:
mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRAAnd in install_db:
mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAMEThose are two hyphens, not one dash. If you have ever pasted a command off a WordPress site and watched it fail, this is why: wptexturize turns --user into an en dash before anyone reads it. 🤡
Two naming conventions in one project
Here is the part that catches people. Plain PHPUnit looks for files ending Test.php. The scaffolded phpunit.xml.dist looks for something else:
<testsuites>
<testsuite>
<directory prefix="test-" suffix=".php">./tests/</directory>
<exclude>./tests/test-sample.php</exclude>
</testsuite>
</testsuites>prefix="test-", so inside a scaffolded WordPress plugin the files are test-movie-library.php, not MovieLibraryTest.php. Same tool, opposite convention, and the only symptom of getting it wrong is zero tests collected. 🔍
Once the install script has run, phpunit from the plugin root runs the suite.
What this does not cover
Mocking, the factories on WP_UnitTestCase, and wiring any of it into CI.
The flag edits above are for a local MySQL that expects a user and password. A socket-authenticated setup will not need them, so try the script unmodified first. 🔌
