Testing
The framework is tested in two places. Its own repository tests every unit and every build error without WordPress. The test consumers test everything that needs WordPress, a browser or a complete build of a theme or plugin. All tests run in wp-test-env, locally and in CI, never against a development site of wp-dev-env.
Which test goes where
Build errors are always tested in the framework: a class that is wrong on purpose cannot live in a consumer, because it would break the consumer's build. Such fixtures carry @phpstan-ignore with the reason. A successful build, and everything a consumer does at run time, is tested in theme-dev-test. Code written to check a feature stays there as a permanent test.
What a consumer does with the plugin – calling its public API, its public template functions, overriding its templates – is tested by the theme's suite; it branches explicitly on the plugin status it reads from the REST API, so the suite passes with and without the plugin.
Test doubles
WordPress is not loaded in unit tests. tests/Stubs/ holds minimal doubles of the WordPress classes and functions the framework uses, with the real signatures of WordPress 7.1, loaded only by the PHPUnit bootstrap; PHPStan checks the tests against these signatures. A double behaves like WordPress where the framework depends on it – for example, wp_delete_post() moves only posts and pages to the trash, as WordPress does – and a comment says which behavior it models. The WP-CLI double (tests/Stubs/wp-cli.php, WP-CLI 2.12.0) is also PHPStan's signature file: add a symbol there with its real signature before the framework uses it.
Browser tests
The browser tests of the consumers follow a few rules, so that they stay reliable when they run in parallel:
- One WordPress instance per Playwright worker.
bin/e2eof wp-test-env passes the URLs of the instances (WP_BASE_URLS); the Playwright configuration picks one per worker byTEST_PARALLEL_INDEX. Tests of different workers never share a site, and a spec never depends on another spec file. - Every test cleans up exactly what it created – posts, media, users – never "delete all".
- No console noise. The fixtures in
tests/e2e/fixtures.jsfail a test on every console warning, console error and page error. A test that expects a message declares it withexpectConsole; a warning WordPress causes with every theme goes intoALLOWED, with the reason and where WordPress causes it. - 30 seconds per test. A test that needs longer sets its own timeout and says why.
- The same suite runs against the release ZIP (
E2E_TARGET=release); only expectations that differ between the modes branch on it. - Every spec is listed in the spec table of the consumer's README.
An unstable test is a bug: find the cause and fix it, instead of retrying. The login test of the settings page, for example, typed into the wrong field because wp-login.php moves the focus 200 ms after loading; it now waits for the focus.
When to run what
The checks are staged, so that development stays fast:
-
While you develop: only the tests of the change – PHPUnit with
--filter, one Vitest file, one spec. -
Before a commit: all linters and builds of the repositories you changed –
composer lint,npm run lint,composer build-manifestandnpm run buildof a changed consumer, the documentation build when pages changed. -
Before a push: the development build – the browser suites of the affected consumers in development mode, with your framework checkout linked into them:
-
At a release: nothing locally. The release workflows build the consumers' ZIPs, run their suites against them and gate the framework (see Pipelines and releases). To reproduce a failed release workflow, run the release pools of the consumer:
Every check passes without errors and without warnings.
Commands
All commands run in the tools image of wp-test-env, from its directory, with the repositories next to it:
npm run lint includes the scripts and styles in docs/, so install docs/ first (npm ci there).
For the browser tests, wp-test-env starts instances, runs the suites and removes the instances again:
A pool file (tests/dev.pools, tests/release.pools of each consumer) describes the instances of a run: which projects are prepared and linked, which themes and plugins (or ZIPs) each pool installs, and whether it runs the browser tests or a script. The package links (link webcodingcat/wp-app-framework=wp-app-framework) replace the framework the consumer installed with your checkout, so the consumers test it without a release. Plugins can run without a theme; then the default theme of WordPress is active. The README of wp-test-env describes the format.
The benchmark
theme-bench measures the cost of the framework per request: a theme built with the framework against the same theme without it, in release mode, in an instance of wp-test-env of its own. It is no part of a release: run it now and then, on request, to make sure the framework does not cost too much, and compare it with the last report – a new database query on the base load or a cost nobody can explain is fixed in the next release. Never run other tests at the same time. Every area of the framework that runs on requests has a scenario there; a new feature gets one (bin/bench --coverage checks it), and existing scenarios stay so reports remain comparable.