testing-wp
Use when writing, running or fixing tests on a Roots/Acorn WordPress project - PHPUnit, Pest, tests/bootstrap.php, WordPress function stubs ("Call to undefined function" in a test), Playwright specs,
SKILL.mdskills/testing-wp/SKILL.md
GitHubTesting a WordPress project on Acorn
Pick the cheapest level that proves the behaviour, run it through the project runner, and report only what you observed.
Runner placeholders ({{runner.test}}, {{runner.node}}, {{runner.wp}}, {{runner.composer}}, {{runner.cache}}) come from the session context. {{runner.test}} is the profile's PHP test command (PHPUnit or Pest). On Lando the default is a tooling command such as lando phpunit or lando pest; lando-wp explains the wiring.
When to use
- Adding tests for a service, provider, view composer, block class, REST endpoint or Blade component.
- A test fails with an undefined WordPress function, a container error or a database error.
- Setting up or running Playwright specs, headless or headed.
- The user asks whether a change is done: this skill defines the minimum evidence.
Not for lint rules or type analysis (wp-phpstan, the project's formatter), and not for diagnosing a broken page that has no test (roots-debugging).
Inputs required
- Profile and Acorn base path (session context).
- Which test tooling the project has:
phpunit.xmlorphpunit.xml.dist,tests/bootstrap.phportests/Pest.php,playwright.config.*, thecomposer.jsonscripts. Read them first; follow the project's conventions rather than introducing a second runner. - Whether a browser can run where you are (containers usually have no display).
- Whether the target environment is local. Tests that hit a real environment, create real accounts or send real mail run only when the user asks.
Test levels per profile
| Level | sage-theme |
acorn-mu-plugin |
radicle-app |
|---|---|---|---|
| Static / lint | formatter and static analysis through their runners | same | project formatter and linter scripts (composer.json scripts) |
| Unit, no WordPress | PHPUnit if the theme has it; keep logic in plain classes under app/ |
PHPUnit with a bootstrap that does not boot WordPress; WordPress functions are stubbed | rarely needed; Pest tests are normally integration |
| Integration, WordPress booted | {{runner.wp}} eval or eval-file smoke scripts |
same, or the WordPress core test library if the project adopts it | Pest, whose bootstrap loads WordPress and Acorn and runs against the configured database |
| HTTP / REST | Playwright request tests or curl |
same | same, or Pest feature tests |
| Browser / visual | Playwright | Playwright | Playwright |
Native-first: for WordPress behaviour (hooks, REST routes, capabilities) the native tool for integration tests is WordPress's own PHPUnit test library with its separate test database. Use it when the project already does or when isolation matters. Do not invent a new in-house harness when one of the existing ones fits.
Procedure
Commands: write every command with the runner value from the session context filled in (for example lando wp acorn optimize:clear), never a bare wp/composer/php/npm, never the placeholder, never "prefix with lando if you use it". Commands that run in CI or a deploy script go in a separate block labelled with where they run.
- Discover. Read the test config and one existing test of the kind you are about to write. Match its naming, namespace and assertions.
- Choose the level. Pure data shaping, validation, middleware rules and services: unit. Anything that needs real hooks, the database or rendered output: integration or browser.
- Unit tests without WordPress (mu-plugin style).
- The bootstrap defines the constants the code reads, registers stubs for WordPress functions before the Composer autoloader (so framework helpers do not win), then loads the autoloader. No WordPress, no Acorn boot.
- When a test needs another WordPress function or class, add a stub to the bootstrap guarded with
function_existsorclass_exists. Do not boot WordPress to avoid writing a stub. - Keep logic in small methods that do not call WordPress directly, so the test needs few stubs. For classes whose constructor needs the framework, create them with
ReflectionClass::newInstanceWithoutConstructor()and test protected helpers throughReflectionMethod. - Details: references/unit-without-wordpress.md.
- Integration tests with WordPress booted (Radicle style). Read references/pest-wordpress.md before writing: these run against a real database.
- Browser tests. Read references/playwright.md: base URL, projects, headed runs on the host, API request tests, visual checks.
- Run. Execute the narrowest command first (a filter on the test class or name), then the whole suite:
{{runner.test}} --filter <Name>then{{runner.test}}. Read the output, not only the exit code. - Before reporting done. Run, in this order, whatever the project defines: formatter/linter check,
{{runner.test}}, the asset build when CSS or JS changed ({{runner.node}} run build), and{{runner.cache}}if PHP registration code changed. State which you ran and the result. - Never claim a pass you did not observe; say so when a level could not run (no browser, no database).
Verification
- The new test fails for the right reason before the fix (when fixing a bug) and passes after.
- The test fails when you break the behaviour on purpose (revert the line, expect red). A test that cannot fail proves nothing.
- The whole suite is green through
{{runner.test}}, not only your file. - The final report lists commands run and results, and names anything skipped and why.
Failure modes
| Symptom | Cause | Fix |
|---|---|---|
Call to undefined function get_option() (or any WordPress function) in a unit test |
The unit bootstrap does not load WordPress | Add a guarded stub in the bootstrap, or move the call behind a small method the test does not exercise |
Framework helper __() or app() behaves unexpectedly in tests |
Stub declared after the Composer autoloader, so the framework helper was defined first | Declare stubs before require vendor/autoload.php |
Target class [x] does not exist or empty container |
Test relies on a booted Acorn application | Build the needed object by hand, or move the test to the integration level |
| Eloquent model fails with no connection | No database in the unit bootstrap | Configure an in-memory SQLite connection in the bootstrap for model scopes; use the booted suite for anything WordPress-specific |
| Integration test changes or deletes real data | The suite runs against the development database | Create and remove your own records inside the test; use a dedicated database where the project provides one; never run destructive tests unasked |
| Pest or PHPUnit not found through the runner | The tool is not installed for that Composer project (nested app has its own vendor/) |
Install it in the right composer.json with --working-dir, or call the runner pointing at the correct config file |
| Playwright cannot launch a browser in the container | No browsers or system libraries installed, no display | Install browsers once, run headless in the container, run headed on the host |
| Playwright passes locally, fails against a remote | Edge or page cache serving old HTML, or different data | Check response headers; use the project's cache-bypass option if it has one |
| Spec creates real accounts or sends data to third parties | A "live" project or spec ran by default | Keep live specs in a project that must be selected explicitly; run only on request |
acorn test is missing |
The installed Acorn does not register a test command in this setup | Call the test runner binary directly through {{runner.test}} |
Escalation
- A test that needs secrets or a real environment: ask the user for the environment and credentials source; never write secrets into specs or
.envfiles. - Changing the CI configuration or adding a new test framework: propose and wait for approval.
- Flaky browser tests: capture a trace and stop after two failed fixes; report the evidence.