Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Monkey Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Brain Monkey lets you unit-test WordPress plugin/theme code without loading WordPress: it stubs WP functions (get_option, apply_filters, esc_html, …) and records the plugin API (add_action, do_action, filters) so you can assert on it. It layers Mockery on top of Patchwork (which rewrites function calls at include time).

Install and wire up

composer require --dev brain/monkey mockery/mockery
// tests/bootstrap.php
require __DIR__ . '/../vendor/autoload.php';
<!-- phpunit.xml.dist -->
<phpunit bootstrap="tests/bootstrap.php" colors="true">
  <testsuites>
    <testsuite name="unit"><directory>tests/unit</directory></testsuite>
  </testsuites>
</phpunit>

Base test case — the first thing to write

namespace MyPlugin\Tests;

use Brain\Monkey;
use PHPUnit\Framework\TestCase;

abstract class UnitTestCase extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();
        Monkey\setUp();          // resets hook storage + Mockery container
    }

    protected function tearDown(): void
    {
        Monkey\tearDown();       // Mockery::close() -> unmet expectations fail here
        parent::tearDown();
    }
}

First use case — a class that reads an option and escapes output:

use Brain\Monkey\Functions;

public function test_renders_label(): void
{
    Functions\when('get_option')->justReturn('Hello');
    Functions\when('esc_html')->returnArg();   // passthrough

    $sut = new LabelRenderer();
    self::assertSame('<span>Hello</span>', $sut->render());
}

Where to look first

  • Brain\Monkey\Functions\when() — zero-verification stubs (what most of your tests need).
  • Brain\Monkey\Functions\expect() — Mockery expectations with call counts/args (->once(), ->with()).
  • Brain\Monkey\Actions / Brain\Monkey\Filters — expectAdded, expectDone, expectApplied, has, applied.
  • Monkey\setUp() / Monkey\tearDown() — the only lifecycle you must not forget.

Start by unit-testing the pieces of your plugin that are pure logic plus a couple of WP calls (option reads, capability checks, translation functions). That is where Brain Monkey pays for itself instantly.

Implementation Patterns

Stub vs. expect — pick deliberately

// "I don't care how often, just return this"
Functions\when('current_user_can')->justReturn(true);

// "It must be called exactly once, with this arg"
Functions\expect('update_option')
    ->once()
    ->with('my_plugin_flag', 1)
    ->andReturn(true);

// "It must never be called on this path"
Functions\expect('wp_die')->never();

when() calls are cheap and don't fail if unused; expect() calls are verified in Monkey\tearDown(). Use when() liberally for the surrounding WP furniture and expect() only for the behaviour the test is actually about.

Conditional / argument-dependent returns

Functions\when('get_option')->alias(function (string $key, $default = false) {
    return ['my_plugin_a' => 'A'][$key] ?? $default;
});

alias() with a closure is the cleanest way to fake a small options table or a settings array without a dozen when() calls.

Testing hook registration

use Brain\Monkey\Actions;

public function test_boot_registers_hooks(): void
{
    Actions\expectAdded('init')->once()->with(\Mockery::type('callable'));
    Actions\expectAdded('save_post')->once();

    (new Plugin())->boot();   // call boot inside the test, not at file scope
}

If you don't want to assert counts up front, run the code and inspect afterwards:

(new Plugin())->boot();

self::assertTrue(Actions\has('init'));
self::assertTrue(Actions\has('save_post', [$plugin, 'onSave']));
self::assertSame(10, Actions\has('init', [$plugin, 'onInit'])); // priority

When the code under test calls has_action() / has_filter() itself

Plugins commonly inspect hooks defensively (e.g. if (! has_action('init', 'other_plugin_boot')) { ... }). Brain Monkey ships function mocks for these hook-inspection functions, so you stub them like any other WP function. Since 2.7.0 the mocks accept the $priority third argument introduced in WordPress 6.9:

// code: has_action('save_post', [$this, 'onSave'], 20)
Functions\when('has_action')->justReturn(true);

// or assert the exact lookup, priority included
Functions\expect('has_action')
    ->once()
    ->with('save_post', \Mockery::type('callable'), 20)
    ->andReturn(false);

The same release aligned the mock signatures of doing_action(), doing_filter(), do_action_deprecated() and apply_filters_deprecated() with WP Core, so code that omits an optional argument (e.g. a bare doing_action()) now behaves as it would in WordPress instead of tripping an argument-count/default mismatch. If you previously worked around those calls with Functions\when('doing_action')->justReturn(...), try removing the workaround.

Testing filters the code applies

use Brain\Monkey\Filters;

Filters\expectApplied('my_plugin_data')
    ->once()
    ->with(['raw']);

// or post-hoc:
$sut->process(['raw']);
self::assertSame(['raw'], Filters\applied('my_plugin_data')[0][0]);

Because apply_filters is a stubbed function, remember its return value is whatever you told it to be — stub it as passthrough unless the test is specifically about filter output:

Functions\when('apply_filters')->returnArg(2); // (hook, $value, ...) -> $value

Testing the callback, not the hook runner

The highest-value pattern is: assert registration once at the "wiring" level, then instantiate the class and call the callback method directly. You get full control of arguments and no dependence on how Brain Monkey executes do_action.

public function test_on_save_post_writes_meta(): void
{
    Functions\expect('update_post_meta')->once()->with(123, '_flag', '1');
    Functions\when('sanitize_text_field')->returnArg();

    (new MetaSaver())->onSave(123);
}

Turn "cannot continue" WP functions into exceptions

Functions\when('wp_die')->alias(function ($msg) {
    throw new \RuntimeException((string) $msg);
});
Functions\when('wp_safe_redirect')->alias(function ($url) {
    throw new \RuntimeException('redirect:' . $url);
});

$this->expectException(\RuntimeException::class);
$sut->handleBadRequest();

This is the standard way to test guard clauses and redirect paths instead of letting the test process die.

Stubbing WP "furniture" once per suite

Translation, escaping and sanitising functions appear everywhere. Brain Monkey ships helpers for the common ones (Functions\stubTranslationFunctions(), Functions\stubEscapeFunctions()); otherwise put your own defaults in the base class:

protected function stubWordPressDefaults(): void
{
    Functions\when('__')->returnArg();
    Functions\when('esc_html')->returnArg();
    Functions\when('esc_attr')->returnArg();
    Functions\when('sanitize_text_field')->returnArg();
    Functions\when('absint')->alias('absint'); // real PHP function, harmless
}

Mix with Mockery for objects

Brain Monkey's Monkey\setUp() initialises the Mockery container, so object mocks and function mocks share one lifecycle:

$wpdb = \Mockery::mock('wpdb');
$wpdb->shouldReceive('get_results')->once()->andReturn([]);
$GLOBALS['wpdb'] = $wpdb;   // remember to unset in tearDown

Functions\when('get_current_user_id')->justReturn(7);

Data providers + fresh state

Because Monkey\setUp() clears hook storage and Mockery expectations, parameterised tests work naturally as long as every case goes through setUp(). Never call Monkey\setUp() twice in one test — it will silently discard expectations you've already declared.

Gotchas and Tips

Upgrading to 2.7.0. No breaking changes to the public API, but two things can bite. (1) The Mockery version constraint was tightened — update brain/monkey and mockery/mockery together (composer update brain/monkey mockery/mockery --with-dependencies); if you've pinned an old Mockery in a legacy plugin, Composer will now refuse the combination, and that's a real signal, not something to bypass. (2) The distributed package no longer ships dev-only files — if you ever poked at the package's own test suite from vendor/, use composer require --dev --prefer-source brain/monkey instead. PHP 8.4 and 8.5 are now supported, so you can raise the PHP matrix in CI with confidence.

Functions must be patched before the calling file is loaded. Patchwork rewrites source at include time. If a bootstrap file requires your plugin classes before Monkey\setUp(), or a polyfill already defined the function, the stub won't take effect. Keep setUp() first, rely on Composer autoloading for classes under test, and avoid require-ing plugin files from phpunit.xml bootstrap.

Internal PHP functions cannot be mocked. Patchwork can't redefine strlen, array_map, json_encode, etc. If you need to control those, wrap them behind your own function/class. The same applies to functions from loaded extensions.

Unstubbed WP functions are fatal, not null. Calling get_option() when it was never stubbed gives "Call to undefined function". When a test blows up with that, it's a missing when() — not a Brain Monkey failure.

apply_filters returning null is the classic trap. Code like $value = apply_filters('x', $value); will silently null your data if you stubbed apply_filters with justReturn('x') or forgot it. Default it to returnArg(2) in your base class.

Use fully-qualified names in when() / expect(). Mock 'My\Plugin\Helper\fetch' exactly as the call resolves (no leading backslash). An unqualified when('fetch') won't match namespaced calls.

has_action() / has_filter() priority is a third argument, and it must match. Since 2.7.0 the mocks accept $priority (WP 6.9 signature). If your with(...) assertion omits it, the expectation matches the call WP would make with the default; if the code under test passes an explicit priority, assert it explicitly or you'll get a confusing with() mismatch rather than a clear failure.

Beware hook registration at include time. If your plugin calls add_action() from the main plugin file and that file was loaded before the test, the registration is gone from this test's hook storage. Either include the file inside the test, or expose a boot() method and call it in the test — the latter is far more testable.

Opcache and Patchwork don't always agree. If stubs mysteriously stop applying after a deploy or a Docker rebuild, disable opcache for the CLI SAPI (opcache.enable_cli=0) or ensure opcache.validate_timestamps=1. Similarly, composer dump-autoload after moving test/bootstrap files.

Expectation ordering and counts. expect() is strict: an extra call fails, a missing call fails at teardown. For optional WP calls (e.g. wp_cache_delete inside a conditional) prefer ->atLeast()->once() or ->zeroOrMoreTimes(), or drop to when() and assert via a flag.

Hook-state functions need their own handling. did_action(), doing_action(), doing_filter() and current_filter() reflect hook execution state, which is separate from the registration bookkeeping Actions\has() inspects. If your code branches on did_action('init'), drive it explicitly (Functions\when('did_action')->justReturn(true)) — don't assume Brain Monkey knows a hook fired just because expectAdded() passed. (The 2.7.0 default-alignment fixes mean the doing_* mocks now tolerate the optional arguments WP Core allows, but they still don't infer state for you.)

Don't test do_action end-to-end here. Whether a registered callback actually fires through do_action depends on Brain Monkey's minimal hook implementation and on call timing. Assert the registration with Actions\expectAdded() in one test, then invoke the callback method directly in another. That split is both more reliable and better documentation of intent.

Debugging a stub that "doesn't work": in order — (1) is the function name fully qualified and correctly spelled? (2) was the calling file included after setUp()? (3) does the function already exist because WordPress or a polyfill was loaded? (4) is opcache caching the rewritten file? Add a temporary Functions\when('the_func')->alias(function () { fwrite(STDERR, "hit\n"); return null; }); to confirm whether the patch is applied at all.

Extension points worth knowing: alias() for closure-based fakes, returnArg($n) for passthroughs, justEcho() for functions whose output is echoed, and Functions\expect(...)->andReturnUsing(...) for Mockery-style dynamic returns. For cross-suite defaults, put them in a trait or the base setUp() rather than copy-pasting into every test.

Scope check. Brain Monkey is for unit tests: no database, no real WordPress, no REST request cycle. When you need real WP behaviour (post meta persistence, capability mapping, REST routes), use wp-env / wp-phpunit integration tests and reserve Brain Monkey for the fast inner loop. Mixing them in one test class is a reliable source of confusing failures.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings