🧪Testing
PHPUnit unit tests and PHPStan static analysis for PressGang's own PHP framework code — no WordPress install required — plus where to go for full theme end-to-end testing.
PressGang ships with a unit test suite so you can verify framework behaviour and safely refactor without a running WordPress installation.
🧰 Stack
PHPUnit 9.6 — test runner
yoast/wp-test-utils ^1.2 — provides BrainMonkey integration and pre-stubbed WordPress functions (matches Timber 2's own test stack)
BrainMonkey — mocks WordPress functions (
add_action,apply_filters,wp_cache_get, etc.) in pure PHP
No WordPress database, no web server, no Docker required.
▶️ Running Tests
composer test # alias for test:unit
composer test:unit # run the full unit suite
composer test:compat # run the unit suite with strict PHP error reporting
composer check # local convenience: test:compat + phpstan
vendor/bin/phpunit --filter ConfigTest # run a single test class
vendor/bin/phpunit --filter loads_and_caches # run a single test by name
vendor/bin/phpunit --list-tests # list all discovered tests🗂️ Directory Structure
Tests mirror the src/ layout under tests/Unit/:
✍️ Writing a New Test
Extend the base TestCase
The base TestCase extends Yoast\WPTestUtils\BrainMonkey\YoastTestCase, which handles BrainMonkey setup and teardown automatically. It also provides:
resetSingletonInstances()— clearsConfigurationSingletonstate between testssetPostData()/clearPostData()— helpers for testing form validators
🔧 Testing Context Managers
Context managers depend on static calls (Timber::get_menu(), new Site()) and global helpers (config()) that cannot be mocked directly with BrainMonkey. PressGang uses the protected method pattern — static calls are wrapped in protected methods that tests override via anonymous subclasses:
This avoids @runTestsInSeparateProcesses (which is 5-10x slower) and keeps tests fast and deterministic.
💡 Tips and Gotchas
BrainMonkey apply_filters signature
apply_filters receives ($hook, $value, ...$extra). To pass through the value unchanged, use the pattern below.
Do not use andReturnFirstArg() — that returns the hook name, not the value.
Pre-loaded functions cannot be mocked
Functions loaded via Composer's files autoload (like the config() helper) are defined before BrainMonkey initialises. Extract calls to these functions into protected methods and override them in tests.
wp_parse_args is pre-stubbed
🔬 Static Analysis
PressGang also runs PHPStan at level 8, so type errors and nullability bugs are caught before a test even needs to exist for them.
szepeviktor/phpstan-wordpress— WordPress core function/class stubsphp-stubs/woocommerce-stubsandphp-stubs/acf-pro-stubs— stubs for the two plugin APIs PressGang integrates with most deeply
composer check is intentionally narrow: it runs test:compat and phpstan, and nothing else. Keep CI jobs split into their existing separate steps so the Actions UI still shows whether tests, compatibility, static analysis, or browser/runtime checks failed.
For controller manifests and getter-backed model access, install the PressGang PHPStan extension in the child theme. Its guide covers installation, automatic checks and optional manifest-omission advice.
🧭 Theme tooling convention
Child themes should use the same shape:
PHPStan level 8 with a local
phpstan.neon.dist.composer phpstanfor static analysis.composer test:compatfor the strict PHP compatibility/unit pass when the theme has tests.composer checkas a local convenience alias fortest:compat+phpstanonly.Project stubs for vendor/runtime type mismatches when they model reality better than an ignore.
Explicit, documented ignores only when a source fix, docblock improvement, or stub would be worse.
Agents should run composer check when it exists. If it does not, run the project's documented test and static-analysis commands separately. Treat PHPStan findings as guidance for improving source, PHPDoc, stubs, or config; do not add baselines or broad ignores unless a maintainer explicitly asks.
A couple of ignored rules in phpstan.neon.dist are architectural, not suppressed bugs — see Known Exceptions in AGENTS.md for why trait.unused and CustomMenuItems.php's dynamic WP_Post properties are ignored.
🧱 Tool boundaries
Static analysis, runtime introspection, and end-to-end verification are separate signals:
PHPStan catches static type, nullability, and convention drift in PHP code.
Capstan provides runtime introspection with
wp capstan resolve,wp capstan context,wp capstan config dump, andwp capstan doctor.Shakedown uses Capstan's
wp capstan matrix --resolveoracle to assert route/controller/runtime behaviour in CI.
Do not design new route, controller, context, or config-dump validation here: those surfaces already exist in Capstan and Shakedown.
The convention extension is separate from shared project stubs and ignore lists. Track repeated extraction candidates — nav-menu WP_Post dynamic properties, WooCommerce cart lifecycle stubs, WooCommerce/ACF stub bundling, and phpstan-bootstrap.php constants — but extract only after a second PressGang repo independently adopts PHPStan level 8 and hits the same needs. One consumer is not a package.
🚢 End-to-end testing
Unit tests cover the framework's PHP in isolation. For testing an actual theme — every route rendered in a real browser, accessibility, visual regression, derived fixtures — see Shakedown, the fleet's e2e harness. It needs zero authored tests to start: the suite is derived from your theme's config.
Last updated