lucatume/wp-browser
wp-browser integrates WordPress with Codeception, giving you acceptance, functional, and integration testing out of the box. It handles WordPress installation, database setup, and bootstrapping so you can write reliable tests for plugins, themes, and full sites.
lucatume/wp-browser is a Codeception add-on that gives you what Laravel gives you out of the box — a booted framework, factories, a database you can assert against, and browser tests — except the framework is WordPress. It is the de-facto standard for plugin/theme testing because it ships its own WordPress test library (wp-phpunit/wp-phpunit), a WP‑aware WPTestCase, and modules (WPLoader, WPDb, WPBrowser, WPWebDriver, WPCLI, WPFunctionMocker) that map cleanly onto concerns you already know.
Minimum viable setup (roughly 15 minutes):
composer require --dev lucatume/wp-browser
vendor/bin/wp-browser init # scaffolds codeception.yml + suites + .env.testing
init writes a tests/ tree with wpunit, functional, and acceptance suites and an .env.testing. Fill in:
WP_ROOT_FOLDER — a real WordPress install on disk (the code under test must be visible as a plugin/theme inside it, usually via symlink)TEST_SITE_DB_NAME / _USER / _PASSWORD / _HOST — a separate database, created beforehand; the user needs CREATE/DROPTEST_SITE_WP_DOMAIN, TEST_SITE_ADMIN_EMAIL, TEST_SITE_WP_URLThen:
vendor/bin/codecept build
vendor/bin/codecept run wpunit
Where to look first: tests/wpunit.suite.yml (that's the WPLoader config) and the generated tests/wpunit/*Test.php. WPLoader is the module that installs WordPress into the test DB and bootstraps it before every run; every other module is a thin helper around it. Read the WPLoader and WPDb module docs before anything else.
First real use case: a plugin activation + option round-trip. Register your plugin via WPLoader's plugins/activatePlugins, then write a WPTestCase that creates a post with the factory and asserts your hook fired. If that passes, you have 80% of the value already.
WPUnit is your RefreshDatabase + feature test. WPLoader installs WP once, then WP_UnitTestCase wraps each test in a DB transaction and rolls back at teardown. You get WordPress's own factories:
class MyPluginTest extends \lucatume\WPBrowser\TestCase\WPTestCase {
public function test_meta_is_saved(): void {
$postId = static::factory()->post->create(['post_type' => 'book']);
update_post_meta($postId, '_isbn', '123');
$this->assertSame('123', get_post_meta($postId, '_isbn', true));
}
}
static::factory()->post|user|term|attachment|comment is the direct analogue of Laravel model factories — use it instead of hand-rolled fixtures.
Module split by test type, not by preference. A practical mapping:
| Need | Suite / module | Analogue |
|---|---|---|
| Business logic with WP loaded | wpunit + WPLoader |
Feature test with RefreshDatabase |
| Hooks, filters, cron, REST controllers | wpunit |
Feature test |
| HTTP-level behaviour, no JS | functional + WPBrowser |
$this->get() / HTTP tests |
| Real browser, JS, wp-admin clicks | acceptance + WPWebDriver |
Dusk |
For REST endpoints, skip the browser entirely: build a WP_REST_Request and call rest_do_request() inside wpunit. It's an order of magnitude faster and asserts the same thing.
Seed with WPCLI, assert with WPDb. WPCLI is the pragmatic fixture layer when WordPress APIs are awkward ($I->cli('plugin activate my-plugin'), $I->cli('post create --post_title=Seed')). WPDb is your DB facade: haveInDatabase, seeInDatabase, grabFromDatabase, haveOptionInDatabase, dontSeeOptionInDatabase. Reach for WPDb for assertions on raw tables (_posts, _options, custom tables) where calling WP functions would just re-test WordPress.
Hooks and filters: set up in _before, tear down in _after. Filters added in a test leak into the next unless you remove them. Either remove_all_filters('my_hook') in _after, or put shared hook wiring in tests/_support/Helper/Wpunit.php so it stays inspectable.
Function mocking is a scalpel, not a hammer. WPFunctionMocker (Patchwork-based) lets you stub wp_remote_get, current_time, etc., but it must load before WP defines those functions — so it belongs in a unit-style suite, not alongside WPLoader in the same suite. Most of the time add_filter with a closure is cheaper and more honest.
CI on a Laravel-shaped repo: add a second job with a MySQL service, composer install, create the test DB, codecept build, codecept run wpunit --no-colors. Cache tests/_data/ and the DB volume — the first WPLoader run is the expensive one. Keep it a separate job from php artisan test so a WordPress dependency problem never blocks your app pipeline.
wp-browser pulls Codeception + wp-phpunit + a PHPUnit version; Laravel's phpunit/phpunit constraint will frequently conflict. Do not fight it — isolate WordPress tests with bamarni/composer-bin-plugin (a tools/wp-browser bin) or a separate tests/wp/composer.json. Treating wp-browser as a second, independent test harness is the only thing that scales across Laravel upgrades.WPLoader installs a full WordPress into TEST_SITE_DB_NAME via wp core install-style bootstrapping. Don't drop the DB between runs and don't share it with your dev database. Expect 30–60s cold, a few seconds warm.CREATE TABLE, TRUNCATE, ALTER TABLE, and anything that implicitly commits will persist across tests. Custom-table plugins need explicit cleanup in _after, or a wpunit suite variant that reinstalls.$this->factory() only exists in WPTestCase. In functional/acceptance you have $I and the modules — no factories. Trying to call $this->factory in a Cept/Cest is the most common beginner error.WP_ROOT_FOLDER must be a genuine WP install with your code discoverable. Symlink wp-content/plugins/my-plugin → your repo. If WPLoader reports the plugin can't be found, it's almost always the symlink or a plugins path relative to the WP root.WP_DEBUG_DISPLAY will poison your assertions. Notices printed into the response break seeInPageSource/seeResponseContainsJson. Set WP_DEBUG=true, WP_DEBUG_DISPLAY=false, WP_DEBUG_LOG=true, and read wp-content/debug.log — it's the fastest debugging loop you have.wp-phpunit/wp-phpunit ships a specific WP version's test suite. Testing against a different WP version than the library expects produces bizarre failures in core test case internals. Bump wp-phpunit and WP_ROOT_FOLDER together.WP_MULTISITE=1 in the environment plus multisite: true in WPLoader. Network activation changes hook firing order; don't assume single-site tests cover it.codecept run wpunit --debug -vvv (shows module interactions), codecept run wpunit --filter test_name, and tests/_output/ for HTML/page-source dumps on acceptance failures.tests/_support/Helper/ for auth/fixture helpers → a custom Codeception\Module wrapping your domain assertions → WPLoader's bootstrapActions and configFile (point at your own wp-tests-config.php) when you need to force constants early. Most teams never need more than a helper.How can I help you explore Laravel packages today?