Skip to content

Smart Add-Ons PHPUnit Testing Without WordPress: 4 Easy Wins

Smart Add-Ons PHPUnit testing without WordPress: hand-written stubs, an in-memory query builder, no test database

Developer guide

How does Smart Add-Ons PHPUnit testing without WordPress actually work?

Plain PHPUnit against a hand-written bootstrap file that stubs the two dozen WordPress functions the plugin's own logic actually calls — sanitize_key, add_action, wp_cache_get, and so on — rather than a full WordPress test install with a real database. Database-touching classes get an in-memory subclass of the plugin's own QueryBuilder instead of a mocking framework, so the class under test runs completely unmodified against array-backed fake tables.

Smart Add-Ons PHPUnit testing without WordPress means every one of the 24 test classes in tests/php/ runs with a plain vendor/bin/phpunit call — no wp-phpunit, no test database, no WordPress core loaded at all. That’s a deliberate trade-off worth understanding before copying the pattern: what it buys, what it can’t cover, and the real stub bug it already caught once. The test classes themselves are loaded through the same Composer PSR-4 autoloading as the rest of the plugin, just under a separate dev-only namespace.

Smart Add-Ons PHPUnit testing without WordPress: hand-written stubs, an in-memory query builder, no test database
A hand-written bootstrap instead of a full WordPress test install.

Smart Add-Ons PHPUnit testing without WordPress: the bootstrap file

phpunit.xml points at a bootstrap that’s just a plain PHP file, not the WordPress core test scaffolding:

<phpunit bootstrap="tests/php/bootstrap.php" colors="true">
    <testsuites>
        <testsuite name="Smart Add-Ons">
            <directory>tests/php</directory>
        </testsuite>
    </testsuites>
</phpunit>

That bootstrap defines around two dozen WordPress functions as plain PHP — sanitize_key, current_time, wp_json_encode, add_action/do_action, wp_cache_get/set/delete, and a handful more — each guarded with function_exists() so nothing conflicts if a real WordPress load ever happens to be present. The hook functions are genuinely minimal: callbacks are stored in a $GLOBALS array in registration order, with priority ignored entirely, which is enough to test that a filter runs and returns the right value without needing WordPress’s actual priority-sorted hook system.

A stub bug the tests caught for real

This isn’t a hypothetical risk — it already happened once, and the fix is documented directly in the bootstrap file’s own comment. The original sanitize_key stub stripped non-lowercase characters before lowercasing, instead of lowercasing first the way real WordPress does:

// Order matters: real WP core lowercases first, then strips - this stub
// used to do it the other way around, which silently dropped any
// uppercase-lettered input entirely ... e.g. sanitize_key('PDF')
// returned '' instead of 'pdf'. Found via UploadValidatorAllowedTypesTest
// asserting real WP's documented behavior and failing against this stub.

A test asserting the real, documented behavior of sanitize_key('PDF') failed against the buggy stub — which is exactly the scenario a hand-rolled stub is supposed to protect against, and in this case did. The fix was one line; the interesting part is that a wrong stub would otherwise have silently made every test that touched file-extension sanitization pass against incorrect behavior indefinitely, with nothing in a WordPress-free test run to ever notice.

Testing database-touching code without a database

Classes like AddonRepository that read and write through the plugin’s own QueryBuilder don’t get mocked — they get a real subclass of the production QueryBuilder with insert()/update()/delete() backed by plain PHP arrays instead of $wpdb:

final class InMemoryQueryBuilder extends QueryBuilder {
    public array $tables = [];
    private array $next_ids = [];

    public function insert( string $table, array $data, ?array $formats = null ): int {
        $id = ( $this->next_ids[ $table ] ?? 0 ) + 1;
        $this->next_ids[ $table ] = $id;
        $this->tables[ $table ][ $id ] = array_merge( [ 'id' => $id ], $data );
        return $id;
    }
}

AddonRepository itself is never modified or mocked to make this work — it’s constructed with the fake QueryBuilder injected in place of the real one, exactly the pattern the container’s own bind()/make() approach is built around. The class under test runs its actual, unmodified logic; only the SQL-executing layer underneath it is swapped.

What this setup genuinely can’t cover

Nothing here tests against a real WordPress database, real hook priority ordering, real capability checks, or the WooCommerce order/cart objects as WordPress actually implements them — only the plugin’s own logic, isolated from WordPress by hand-written stand-ins. That’s the honest boundary of Smart Add-Ons PHPUnit testing without WordPress: it’s fast and catches logic bugs early, but it’s not a substitute for testing against a real WordPress install before a release, which is a separate, manual step this test suite doesn’t replace.

Common questions

Why not use wp-phpunit or WP_UnitTestCase instead?

Those bring a full WordPress install and test database into every run, which is thorough but slow and requires real setup (a MySQL instance, WordPress core checked out). Smart Add-Ons PHPUnit testing without WordPress trades that coverage for tests that run instantly with nothing installed beyond Composer’s own dependencies — a reasonable choice for logic-heavy classes like pricing calculators and validators that don’t actually need WordPress running to be correct.

Does this test suite run in CI?

Not currently wired into an automated pipeline as of this writing — it’s run locally via vendor/bin/phpunit. Adding it to CI would be a natural next step given how cheap each run already is, but that’s a real gap worth stating plainly rather than assuming it exists.

For the container that makes swapping a real dependency for a fake one straightforward, see the architecture deep-dive, and PHPUnit’s own documentation (opens in new tab) for the testing framework this all runs on.

Dhyan Software Avatar

Written by

Ready to ship this in your store?

Smart Add-Ons for WooCommerce turns what you just read into a working product page — no code required.

Comments

Leave a comment

Your email is not published.