brain/monkey
Brain Monkey is a PHP testing library for mocking and spying on global functions and class methods, with seamless PHPUnit integration and no dependencies—ideal for testing legacy code, WordPress plugins, and function-heavy PHP applications.
Release delta (2.7.0 vs. 2.6.2). Minor, non-breaking, no deprecations. Five changes matter to us, one of them directly:
has_filter() / has_action() mocks now accept $priority (tracking WordPress 6.9). Previously we could not express "a callback was registered at priority 20" — a common, real hook pattern. This closes a genuine assertion gap rather than adding a convenience.doing_action(), doing_filter(), do_action_deprecated(), apply_filters_deprecated(), has_filter(), has_action(). This is the one change that can turn our own tests from green to red: any test written against the old, incorrect defaults (e.g. omitting a parameter that Core allows to be omitted) may now need editing. The change itself is a correctness fix that shrinks the "false green" surface.include; welcome, not decisive.What this actually is. brain/monkey is not a Laravel package and has no Laravel-specific surface. It is a unit-testing utility for WordPress-oriented PHP: it wraps Mockery to let you (a) replace global/namespaced PHP function calls with controlled doubles and (b) assert against the WordPress hook system (add_action, add_filter, do_action, apply_filters) without loading WordPress. It is the modern successor to the 10up/wp_mock pattern and is the de-facto standard in the WP plugin ecosystem (WP-CLI, Yoast, and most serious plugin vendors use it or its conventions).
Architecture fit. Fit is binary, not gradient — and 2.7.0 does not change this:
wp_remote_get, wp_schedule_event, get_option, time, file_get_contents). It lets that code be unit-tested in milliseconds without a WP runtime.Http::fake(), Queue::fake(), and plain Mockery already cover every case this solves, with better ergonomics and no global-state hacks. The new $priority assertion support is WordPress-hook semantics specifically; it is not a reason to introduce hook mocking into a Laravel service.Integration feasibility. Unchanged and still low effort, high constraint density. Install is composer require --dev brain/monkey; the API is small (Monkey\setUp()/tearDown(), Functions\expect/when, Actions\expectAdded, Filters\expectApplied, Hooks\did_action). The real cost remains bootstrap ordering: the 2.x line intercepts function calls by rewriting files at include time, so the library must be initialized before the code under test is loaded, and functions in already-loaded files cannot be redefined. That is a "read the docs once, then never again" tax — but it silently produces confusing failures when violated. 2.7.0 is a drop-in for 2.6.x; adopt it as a patch-level bump, not a migration.
Technical risk.
opcache.preload, and some CI container images are known friction points; this must be proven in our CI, not assumed. PHP 8.4/8.5 confirmation reduces the version-support unknown but does not substitute for the opcache/coverage proof.$priority support removes a "can't assert this, so assert nothing" gap. The residual risk is unchanged, though: Functions\when('x')->justReturn(null) and unstubbed-expectation defaults can still make a test pass while production behaviour diverges. This remains a convention problem, not a library problem.tearDown() runs; PHPUnit processIsolation/Paratest sidestep it, in-process parallelism does not.Key questions for the spike (timebox: 1 day).
composer update time on a real branch.has_filter/has_action or rely on the previously-wrong default arguments? Those are the upgrade blast radius — identify them before bumping.wp-env, wp-browser)? If yes, this is a complement, not a replacement — we must define the unit/integration boundary explicitly.include? Still yes-leaning — it is dev-only, never ships, and 2.7.0's dist slimming further reduces the shipped-surface question.Stack fit and placement. Do not put this in the Laravel application's test suite. Put it in a separate Composer package — either a path repository in a monorepo or a standalone plugin repo — with its own phpunit.xml, its own autoload root, and its own composer.json dev dependencies. The Laravel app then consumes the plugin package as a normal vendor dependency (or via HTTP, if the plugin only talks to our API). This keeps the stream-wrapper bootstrap out of the app's test lifecycle entirely, which is the single most important architectural decision here.
Sequencing.
^2.7. Success criterion: green build with coverage enabled, no opcache complaints, and zero Mockery constraint conflicts.tests/bootstrap.php that calls Monkey\setUp() in a PHPUnit @before/Pest beforeEach hook and asserts tearDown() runs; wrap in a base TestCase so no test can forget.Functions\when(...)->justReturn(...) is for collaborator stubs; Functions\expect(...)->once() is required for anything with a side effect. Always pass $priority explicitly when asserting has_filter/has_action." The second sentence is new with 2.7.0 and is where the added capability actually converts into quality.Migration path. From 10up/wp_mock the translation is mechanical: WP_Mock::userFunction('x', ['return' => 1]) → Functions\when('x')->justReturn(1); WP_Mock::expectActionAdded(...) → Actions\expectAdded(...); WP_Mock::wpFunction(...)->times(1) → Functions\expect(...)->once(). Doing it file-by-file alongside other work (no dedicated migration sprint) is realistic for a normal-sized plugin. From "no tests at all," start with the pure classes and only reach for this where globals actually block testability.
Upgrading from 2.6.x. Treat as a routine patch bump with one verification pass: composer update brain/monkey, then run the full suite and inspect any test touching doing_action, doing_filter, do_action_deprecated, apply_filters_deprecated, has_filter, or has_action. Failures there are expected and indicate tests that encoded the pre-2.7 (incorrect) defaults. Budget well under a day; no dedicated project.
Compatibility notes. PHPUnit-agnostic and Pest-compatible; requires Mockery. Pin ^2.7 (not ^2.6, to get the corrected defaults and $priority support) and lock Mockery to the major we already use elsewhere to avoid two Mockery majors in one vendor tree — but validate that against 2.7.0's tightened constraints before committing to the pin. PHP 8.4/8.5 are now upstream-supported; add them to the CI matrix.
Maintenance. Near-zero steady state. Upstream churn is low, semantic versioning is respected, and the API is small enough that a breaking change is cheap to absorb. 2.7.0 is consistent with that pattern: additive, correctness-oriented, contributor-diverse. The genuine maintenance cost is our own convention discipline: the library is only as good as the stub contracts we write around it. Budget a standing PR-review rule rather than ongoing engineering time.
Support. Community-only — GitHub issues, no vendor, no SLA. Acceptable for a dev-only test dependency. Mitigate bus factor by pinning exactly, vendoring the fork path in our notes (MIT permits it; the glue is small), and keeping the pilot reproducible so a future maintainer can re-derive decisions from the ADR.
Scaling. Negligible per-test cost; the include-time rewriting adds a small constant to file loading, invisible at plugin-code scale. Concurrency is fine under process-per-worker runners (Paratest, PHPUnit --process-isolation); it is not safe for in-process parallel or long-lived test workers, so don't reach for it if we ever move to a persistent-process test runner. Scaling the number of tests is cheap; scaling the number of teams using it is where the convention doc earns its keep.
Failure modes to instrument against.
| Mode | Symptom | Mitigation |
|---|---|---|
| Bootstrap ordering | "Call to undefined function" or real WP function used | Base TestCase enforces setUp() first; fail loudly |
| Unstubbed expectation | Test green, production diverges | Lint rule / review checklist: side-effecting calls need expect() |
| Hook leakage | Order-dependent flaky tests | tearDown() in base class; run suite with --order-by=random in CI |
| Rewriting incompatibility | Coverage driver errors, silent non-interception | Prove in CI image during pilot; gate on it |
| Namespaced function mismatch | Stub never invoked | Convention: always stub fully-qualified names |
| Mock-default drift on upgrade | Tests fail (or silently pass) after a mock-behaviour fix like 2.7.0's | Run full suite on the bump; review any test touching the six affected functions; assert $priority explicitly |
Ramp-up. One to two days for a developer already fluent in Mockery/PHPUnit; the only genuinely new concepts are bootstrap ordering and the hook assertion API. Plan a single brown-bag session and a 30-line example test as the onboarding artifact — that is sufficient. Do not schedule formal training.
Recommendation. Adopt conditionally and narrowly: approve only if we own WordPress-side code, and only as a dev dependency inside a separately-packaged plugin test suite. Reject outright for Laravel application code. Pin ^2.7 — it is a low-risk correctness upgrade that retires part of the CI-matrix unknown, removes a false-green class, and enables priority-aware hook assertions we previously could not express. Gate adoption on the one-day CI spike that proves coverage-driver and opcache compatibility; that remains the only finding that can invalidate the whole evaluation.
How can I help you explore Laravel packages today?