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

O Testbench Laravel Package

wpstarter/o-testbench

WpStarter port of Orchestra Testbench, providing a Laravel-style testing harness for package development so you can write integration tests for your WpStarter packages without booting a full application.

View on GitHub
Deep Wiki
Context7

Getting Started

Version context: v2.0 is out

A new major tag (v2.0, published 2023-03-23) replaces v1.0. The upstream release notes for v2.0 do not enumerate changes — they only link to the diff:

https://github.com/wpstarter/o-testbench/compare/v1.0...v2.0

Because this is a major bump, treat it as potentially breaking until proven otherwise. Read that compare view (and the UPGRADING/CHANGELOG file in the repo if present) before you let a composer update pull it into CI. Everything below describes the v1.x feature surface that the package exposes; where a detail is version-sensitive it is flagged.

Minimal setup

  1. Install (pin the major to avoid surprise upgrades)

    composer require --dev "wpstarter/o-testbench:^2.0"
    

    If you are not ready to migrate, pin to the previous major:

    composer require --dev "wpstarter/o-testbench:^1.0"
    

    The package is a dev dependency; it belongs in require-dev, and it expects wpstarter/framework to be present.

  2. Register the PHPUnit extension and pin a WP version

    <phpunit>
        <extensions>
            <extension class="WpStarter\TestBench\TestBenchExtension"/>
        </extensions>
        <php>
            <server name="WP_VERSION" value="6.0"/>
            <server name="APP_ENV" value="testing"/>
        </php>
    </phpunit>
    

    WP_VERSION is read by the bootstrap and is required from v1.0 onward; an unset value is the single most common cause of a mysteriously empty WordPress bootstrap.

  3. First test — prove the harness booted before writing real tests

    use WpStarter\TestBench\TestCase;
    
    class SmokeTest extends TestCase
    {
        public function test_environment_boots(): void
        {
            $this->assertTrue(true);
            $this->assertEquals('6.0', $this->wpVersion());
        }
    }
    

    If this passes, the extension, constants, and version wiring are all sound. If it fails, stop and fix the harness — every subsequent failure will be noise.

  4. Configure the environment in getEnvironmentSetUp()

    protected function getEnvironmentSetUp($app): void
    {
        $app['config']->set('database.default', 'sqlite');
        $app['config']->set('database.connections.sqlite.database', ':memory:');
    
        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
        $this->activatePlugin('my-plugin');
    }
    

    This is the hook you will touch most; it runs before each test's application is ready, so it is the right place for config, plugins, themes, and constants — and the wrong place for assertions.

Where to look first

  • WpStarter\TestBench\TestCase — the base class; read its setUp/tearDown to know exactly what is reset between tests.
  • getEnvironmentSetUp() — your primary override point.
  • The TestBenchExtension — confirms which PHPUnit lifecycle hooks are wired.
  • The v1.0→v2.0 compare diff — the only authoritative source of what changed in this release.

First realistic use case

You have a WPStarter plugin with a service provider, a couple of filters, and a REST route. The fastest path to value:

  1. Smoke test (above).
  2. One test asserting your service provider registers ($this->assertTrue($app->providerIsLoaded(MyProvider::class)) or an equivalent container/hook assertion).
  3. One test hitting your REST route via $this->call('GET', '/wp-json/my-plugin/v1/thing').
  4. One database test against wp_posts.

That set proves the harness, the container, routing, and the DB layer — the four things that break most often.


Implementation Patterns

Database testing

Use SQLite in-memory unless you specifically need MySQL behaviour (collations, ON DUPLICATE KEY, full-text):

$app = $this->createApplication([
    'database' => [
        'default' => 'testing',
        'connections' => [
            'testing' => [
                'driver'   => 'sqlite',
                'database' => ':memory:',
                'prefix'   => '',
            ],
        ],
    ],
]);

WordPress table names are prefixed (wp_posts, wp_options, …). The package ships a WordPress-aware assertDatabaseHas():

$this->assertDatabaseHas('wp_posts', ['post_title' => 'Test']);

Keep one canonical helper for table names so a non-default $table_prefix does not silently break half your suite.

Plugin and theme activation

Activate inside getEnvironmentSetUp(), not in the test body — activation is environment setup, and doing it lazily produces order-dependent flakiness:

$this->activatePlugin('my-plugin');
$this->activateTheme('my-theme');
$this->deactivatePlugin('unwanted-plugin');

The helpers return $this in v1.x, so chaining expresses load order explicitly:

$this->activatePlugin('plugin-a')->activatePlugin('plugin-b');

Mocking WordPress functions

$this->mockFunction('get_current_user_id', fn () => 1);
$this->mockFunction('current_user_can', fn () => true);
$this->mockFunction('register_block_type', fn () => true); // Block Editor surface

Two rules that save hours:

  • Reset between tests. resetMocks() exists for exactly this; call it in tearDown() or rely on the base class doing it — verify which, once.
  • Do not mix mockFunction() and WP_Mock::userFunction() for the same symbol. They use different interception layers and the last one wins non-deterministically. Pick one per file.

HTTP / REST testing

$response = $this->call('GET', '/wp-json/wp/v2/posts');
$this->assertEquals(200, $response->status());

$response = $this->call('POST', '/wp-json/wp/v2/blocks', ['title' => 'X']);

Assert on status and on a decoded body fragment; a 200 with a WP_Error payload is a real and common failure mode.

WP-CLI / command testing

$this->artisan('wpstarter:command')
     ->expectsQuestion('confirm', 'yes')
     ->assertExitCode(0)
     ->assertOutputContains('Success');

expectsQuestion is order-sensitive — if you add a prompt, every existing test that asserts questions must be updated in the same order.

Block Editor testing

$this->assertBlockRegistered('core/paragraph');
$this->assertBlockOutput('core/paragraph', '<p>Test</p>');

Block Editor state is process-global. Opt in per test rather than globally:

public function test_block_registration(): void
{
    $this->withBlockEditor();
    $this->assertBlockRegistered('my-plugin/callout');
}

Composing a project base class

Put cross-cutting concerns in one subclass and never repeat them:

abstract class PluginTestCase extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();
        $this->mockFunction('current_user_can', fn () => true);
    }

    protected function getEnvironmentSetUp($app): void
    {
        parent::getEnvironmentSetUp($app);
        $this->activatePlugin('my-plugin');
    }
}

TestBench + WPStarter synergy

The leverage point is that WPStarter bootstraps like Laravel, so you can test:

  • Service providers registering/unregistering via getEnvironmentSetUp().
  • Custom hooks and filters — register in the test, fire with do_action/apply_filters, assert on the return.
  • Migrations — run Laravel migrations against WordPress tables.
  • Blocks and their settings, e.g. assertBlockEditorSetting('core/block-editor', 'allowedBlockTypes', [...]).

Parallel testing

Parallelism and WordPress globals are enemies. Isolate by giving each process its own DB name/domain rather than relying on :memory: if any test writes files:

<phpunit>
    <extensions>
        <extension class="WpStarter\TestBench\TestBenchExtension"/>
    </extensions>
    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="DB_DATABASE" value=":memory:"/>
        <server name="WP_TESTS_DOMAIN" value="example.test"/>
    </php>
</phpunit>

Turn parallel runs on only after the serial suite is green; parallel debugging of shared WordPress state is not worth it.


Gotchas and Tips

Pitfalls

1. Version drift in CI

  • Issue: ^1.0 silently becomes ^2.0 in a lockfile-less environment, or a contributor's composer update pulls v2.0 mid-sprint.
  • Fix: commit composer.lock, and be explicit about the major you target (^2.0 or ^1.0). Re-read the v1.0→v2.0 compare before bumping; the release notes give you nothing else to go on.

2. Missing WordPress constants

  • Issue: tests fail with undefined-constant errors or an empty bootstrap because WP_DEBUG, WP_TESTS_DIR, or WP_VERSION are unset.
  • Fix: define them early, in getEnvironmentSetUp() or a bootstrap file loaded before the suite:
    define('WP_DEBUG', true);
    define('WP_TESTS_DIR', __DIR__);
    define('WP_VERSION', '6.0');
    
    Prefer defined() || define() so a second definition does not warn.

3. Plugin/theme load order

  • Issue: plugin B's plugins_loaded callback runs before plugin A registered the hook it depends on.
  • Fix: chain activations explicitly ($this->activatePlugin('plugin-a')->activatePlugin('plugin-b')) and assert the resulting hook state in a dedicated ordering test.

4. Singletons (wpdb, $wp_filter, $current_user)

  • Issue: core globals persist across the process, so a test that mutates $current_user poisons the next test.
  • Fix: reset in tearDown(); use resetMocks() for mocked functions and wp_set_current_user(0) (or a helper equivalent) for user state. Where you need a truly isolated symbol, WP_Mock::userFunction() is an alternative — but do not mix interception strategies for one function.

5. Static analysis and IDE false positives

  • Issue: PHPStan/Psalm report Call to undefined method on TestBench helpers.
  • Fix: v1.0+ ships PHPDoc blocks, so most IDEs resolve them; for PHPStan, add the package's stubs to paths or ignore the base TestCase if the stubs are incomplete for the helpers you use.

6. Block Editor bleed

  • Issue: enabling block editor mode globally changes registry behaviour for unrelated tests.
  • Fix: scope it with withBlockEditor() inside the tests that need it, and call disableBlockEditorCache() when registration assertions go stale between tests.

7. PHP / Laravel floor

  • Issue: v1.0 required PHP 8.0+ and Laravel 8.0+; a v2.0 major may raise those floors.
  • Fix: check composer.json require of the installed tag and align your CI matrix before upgrading. A failing composer install on 8.0 is the usual first symptom.

Debugging tips

1. Dump the WordPress state wholesale

$this->dumpWordPressState(); // globals, hooks, and more

Reach for this before scattering var_dump() calls; it answers "what did the bootstrap actually load?"

2. Inspect what a mock received

$this->assertEquals(
    ['key' => 'value'],
    WP_Mock::userFunction('get_option')->getLastCallArgs()
);

Useful when a filter chain returns the right value for the wrong reason.

3. Slow suites

  • Causes: real DB connections, filesystem writes, repeated loadMigrationsFrom.
  • Fixes: :memory: SQLite; $app['db']->disableTransactions() only if you have another isolation strategy; disableBlockEditorCache() for block-heavy suites. Profile with --log-junit before optimising blindly.

4. "Class not found"

  • Fix: confirm both wpstarter/o-testbench and wpstarter/framework are installed, then composer dump-autoload. Autoload staleness after a major upgrade is far more common than a genuinely missing class.

5. Block registration looks empty

  • Fix: assert the editor is loaded (assertBlockEditorLoaded()) and settings are coherent (assertBlockEditorSettingsValid()) before asserting individual blocks — otherwise you debug the registry when the problem is activation order.

Extension points

1. Custom assertions Domain assertions belong in your base class, not copy-pasted:

class PostTestCase extends TestCase
{
    protected function assertPostExists(int $id): void
    {
        $this->assertDatabaseHas('wp_posts', ['ID' => $id]);
    }

    protected function assertBlockRegistered(string $name): void
    {
        $this->assertTrue(has_block($name));
    }
}

2. Factories for WordPress objects Laravel factories do not model WP_Post/WP_User well; wrap them instead:

protected function createPost(array $attrs = []): int
{
    return wp_insert_post(array_merge([
        'post_title'  => 'Test',
        'post_status' => 'publish',
        'post_type'   => 'post',
    ], $attrs));
}

3. Overridable hooks in your base class Expose protected function plugins(): array / protected function theme(): ?string in your PluginTestCase so individual tests can declare what they need instead of mutating state mid-test.

4. Filter-based seams For code that is hard to mock (core singletons), add a WPStarter filter around the call and have tests drive the filter. This is usually cheaper — and less brittle across a major upgrade — than expanding your mock surface.

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.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels