WordPress Plugin Testing with WP_Mock and Brain\Monkey

WordPress Plugin Testing with WP_Mock and Brain\Monkey

WordPress plugin unit tests don't need a running WordPress install. WP_Mock and Brain\Monkey let you mock WordPress functions, hooks, and globals so your PHPUnit tests run in isolation — fast, deterministic, and CI-friendly. This guide shows how to set up both tools and write meaningful tests for plugin business logic.

Key Takeaways

Use WP_Mock or Brain\Monkey to test plugin logic without WordPress. Loading the full WordPress environment in unit tests is slow and fragile. Mocking WordPress functions isolates your plugin code.

Test hooks separately from logic. Verify that add_action and add_filter calls register the right callbacks, then test the callbacks independently.

Mock only what you call. Don't mock everything WordPress exports — only the functions your code actually invokes. Unnecessary mocks hide bugs.

Brain\Monkey is better for modern PHP. If your plugin uses Composer and follows PSR standards, Brain\Monkey integrates more cleanly than WP_Mock.

Integration tests still require WordPress. WP_Mock tests your logic. WP-CLI's wp test or the wordpress/wordpress Docker image is needed for integration-level tests that touch the database.

The Problem with WordPress Testing

WordPress plugins live inside a global environment. Functions like get_option(), add_action(), wp_send_json_response(), and current_user_can() are all globals that only exist when WordPress is loaded. Writing tests that load WordPress is possible but slow — bootstrapping takes seconds, database setup adds complexity, and tests become environment-dependent.

The solution: mock the WordPress layer. Two libraries dominate this space:

  • WP_Mock — purpose-built for WordPress function and hook mocking
  • Brain\Monkey — broader PHP function mocking built on Mockery, with WordPress-specific helpers

Both allow you to write PHPUnit tests that run in milliseconds, without a database, without WordPress files.

Setting Up WP_Mock

Install via Composer:

composer require --dev 10up/wp_mock phpunit/phpunit

Create phpunit.xml in your plugin root:

<?xml version="1.0"?>
<phpunit bootstrap="tests/bootstrap.php" colors="true">
    <testsuites>
        <testsuite name="Plugin Tests">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

Create tests/bootstrap.php:

<?php
require_once __DIR__ . '/../vendor/autoload.php';

WP_Mock::bootstrap();

Your test classes extend WP_Mock\Tools\TestCase:

<?php
use WP_Mock\Tools\TestCase;

class MyPluginTest extends TestCase {
    public function setUp(): void {
        parent::setUp();
        WP_Mock::setUp();
    }

    public function tearDown(): void {
        WP_Mock::tearDown();
        parent::tearDown();
    }
}

Setting Up Brain\Monkey

Brain\Monkey uses Mockery under the hood and provides WordPress-specific helpers:

composer require --dev brain/monkey phpunit/phpunit mockery/mockery

Base test class:

<?php
use Brain\Monkey;
use Brain\Monkey\Functions;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;

abstract class PluginTestCase extends \PHPUnit\Framework\TestCase {
    use MockeryPHPUnitIntegration;

    protected function setUp(): void {
        parent::setUp();
        Monkey\setUp();
    }

    protected function tearDown(): void {
        Monkey\tearDown();
        parent::tearDown();
    }
}

Testing Plugin Functions with WP_Mock

Consider a plugin function that reads an option and applies a filter:

<?php
function myplugin_get_api_key(): string {
    $key = get_option('myplugin_api_key', '');
    return apply_filters('myplugin_api_key', $key);
}

The WP_Mock test:

<?php
class ApiKeyTest extends WP_Mock\Tools\TestCase {
    public function test_returns_stored_option(): void {
        WP_Mock::userFunction('get_option', [
            'args'   => ['myplugin_api_key', ''],
            'return' => 'sk-test-12345',
            'times'  => 1,
        ]);

        WP_Mock::onFilter('myplugin_api_key')
            ->with('sk-test-12345')
            ->reply('sk-test-12345');

        $result = myplugin_get_api_key();

        $this->assertSame('sk-test-12345', $result);
        $this->assertConditionsMet();
    }

    public function test_returns_empty_when_option_not_set(): void {
        WP_Mock::userFunction('get_option', [
            'args'   => ['myplugin_api_key', ''],
            'return' => '',
        ]);

        WP_Mock::onFilter('myplugin_api_key')
            ->with('')
            ->reply('');

        $result = myplugin_get_api_key();

        $this->assertSame('', $result);
    }
}

assertConditionsMet() verifies that all declared mock expectations were actually called. This catches code paths that skip expected WordPress function calls.

Testing Hooks with WP_Mock

A common plugin pattern is registering hooks in a constructor or init method:

<?php
class MyPlugin {
    public function __construct() {
        add_action('init', [$this, 'init']);
        add_action('wp_enqueue_scripts', [$this, 'enqueue_scripts']);
        add_filter('the_content', [$this, 'filter_content'], 10, 1);
    }
}

Testing hook registration:

<?php
class MyPluginHooksTest extends WP_Mock\Tools\TestCase {
    public function test_hooks_registered_on_construct(): void {
        WP_Mock::expectActionAdded('init', [
            \Mockery::type(MyPlugin::class), 'init'
        ]);

        WP_Mock::expectActionAdded('wp_enqueue_scripts', [
            \Mockery::type(MyPlugin::class), 'enqueue_scripts'
        ]);

        WP_Mock::expectFilterAdded('the_content', [
            \Mockery::type(MyPlugin::class), 'filter_content'
        ], 10, 1);

        new MyPlugin();

        $this->assertConditionsMet();
    }
}

This verifies priorities and argument counts, not just that the hook was added.

Testing with Brain\Monkey

The same function tested with Brain\Monkey:

<?php
class ApiKeyBrainTest extends PluginTestCase {
    public function test_returns_stored_option(): void {
        Functions\expect('get_option')
            ->once()
            ->with('myplugin_api_key', '')
            ->andReturn('sk-test-12345');

        Functions\expect('apply_filters')
            ->once()
            ->with('myplugin_api_key', 'sk-test-12345')
            ->andReturn('sk-test-12345');

        $result = myplugin_get_api_key();

        $this->assertSame('sk-test-12345', $result);
    }
}

Brain\Monkey's Functions\expect() uses Mockery's fluent interface, which many PHP developers find more readable than WP_Mock's array-based config.

Mocking WordPress Globals

Some plugins interact with WordPress globals like $wpdb or $wp_query. Brain\Monkey handles these via standard PHP variable injection or Mockery:

<?php
public function test_queries_database(): void {
    global $wpdb;

    $wpdb = Mockery::mock('wpdb');
    $wpdb->prefix = 'wp_';
    $wpdb->shouldReceive('get_results')
        ->once()
        ->with("SELECT * FROM wp_myplugin_events WHERE status = 'active'")
        ->andReturn([
            (object)['id' => 1, 'name' => 'Event One'],
        ]);

    $events = myplugin_get_active_events();

    $this->assertCount(1, $events);
    $this->assertSame('Event One', $events[0]->name);
}

Testing REST API Endpoints

WordPress plugins often register REST endpoints. Test the callback logic in isolation:

<?php
class RestEndpointTest extends PluginTestCase {
    public function test_returns_data_for_authenticated_user(): void {
        Functions\expect('current_user_can')
            ->once()
            ->with('read')
            ->andReturn(true);

        Functions\expect('get_posts')
            ->once()
            ->andReturn([
                ['ID' => 42, 'post_title' => 'Test Post'],
            ]);

        $request = Mockery::mock(\WP_REST_Request::class);
        $request->shouldReceive('get_param')
            ->with('per_page')
            ->andReturn(10);

        $response = myplugin_rest_get_posts($request);

        $this->assertIsArray($response);
        $this->assertCount(1, $response);
        $this->assertSame(42, $response[0]['ID']);
    }

    public function test_returns_403_for_unauthenticated_user(): void {
        Functions\expect('current_user_can')
            ->once()
            ->with('read')
            ->andReturn(false);

        Functions\expect('wp_send_json_error')
            ->once()
            ->with(Mockery::any(), 403);

        $request = Mockery::mock(\WP_REST_Request::class);

        myplugin_rest_get_posts($request);
    }
}

Structuring Plugin Code for Testability

Plugins written as procedural files with global functions are hard to test. Refactoring to classes with dependency injection makes mocking easier:

Hard to test:

<?php
function myplugin_send_notification($user_id) {
    $email = get_user_meta($user_id, 'notification_email', true);
    wp_mail($email, 'Subject', 'Body');
}

Testable:

<?php
class NotificationService {
    private $mailer;

    public function __construct(callable $mailer = null) {
        $this->mailer = $mailer ?? 'wp_mail';
    }

    public function send(int $user_id): bool {
        $email = get_user_meta($user_id, 'notification_email', true);
        return ($this->mailer)($email, 'Subject', 'Body');
    }
}

In tests, pass a mock $mailer instead of relying on wp_mail:

<?php
public function test_sends_notification(): void {
    Functions\expect('get_user_meta')
        ->once()
        ->with(42, 'notification_email', true)
        ->andReturn('user@example.com');

    $sent = [];
    $mailer = function($to, $subject, $body) use (&$sent) {
        $sent[] = compact('to', 'subject', 'body');
        return true;
    };

    $service = new NotificationService($mailer);
    $result = $service->send(42);

    $this->assertTrue($result);
    $this->assertCount(1, $sent);
    $this->assertSame('user@example.com', $sent[0]['to']);
}

Running Tests in CI

Add a Makefile target or CI step:

# .github/workflows/test.yml
name: Plugin Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
      - run: composer install --prefer-dist --no-progress
      - run: ./vendor/bin/phpunit

For plugins that also need integration tests against a real WordPress install, use the johnpbloch/wordpress Composer package or the official WordPress Docker image alongside your unit tests.

What to Test

Unit tests with WP_Mock/Brain\Monkey are best for:

  • Business logic (calculations, transformations, validations)
  • Hook registration and priority
  • REST callback authorization and response shape
  • Option reading/writing logic
  • Error handling and fallback behavior

Leave for integration tests:

  • Database queries via $wpdb with real data
  • Plugin activation/deactivation hooks
  • End-to-end REST endpoint responses
  • Shortcode rendering with real WordPress content

Connecting to End-to-End Testing

WP_Mock and Brain\Monkey cover unit-level logic. For end-to-end validation — verifying that your plugin's frontend behavior works correctly for real users — tools like HelpMeTest automate browser-level test scenarios using plain English. You can test plugin UIs, admin pages, and user flows without writing Playwright or Selenium code directly.

The fastest path to confident WordPress plugin releases combines unit coverage (WP_Mock), integration tests (WordPress Docker), and end-to-end monitoring for the user-facing behaviors that matter most.

Read more