pestphp/pest-plugin-init
A Pest plugin that adds an init command to quickly bootstrap Pest in an existing PHP or Laravel project, generating the baseline test configuration and scaffolding needed to start writing Pest tests.
Treat this as a one-shot bootstrapper you run once, inspect, and immediately remove — not a retained dev dependency. The command writes the Pest scaffolding into your project: tests/Pest.php, a base TestCase, the tests/Feature/tests/Unit layout, and the autoload/config wiring in phpunit.xml and composer.json.
Minimum viable flow on a fresh or existing project:
# 1. Pest itself must be present first — the plugin scaffolds, it does not install Pest
composer require --dev pestphp/pest
# 2. Pull the init plugin
composer require --dev pestphp/pest-plugin-init
# 3. Run the scaffolding command (Laravel app)
php artisan pest:init
# ...or standalone PHP project
./vendor/bin/pest --init
# 4. Inspect the diff, prove the suite runs, then remove the plugin
git diff
./vendor/bin/pest
composer remove --dev pestphp/pest-plugin-init
Before you run it:
phpunit.xml, composer.json, and adds files under tests/. A dirty tree makes the diff impossible to review, and review is the whole point of running this on legacy trees.pestphp/pest is installed and pinned to whatever line your repo supports. On Pest 2.x/3.x the scaffolding is already in core; use ./vendor/bin/pest --init and skip the plugin entirely.composer why-not pestphp/pest before upgrading Pest on any repo where this is pinned — if the plugin is the blocker, that is your signal to remove it, not to fork it.Where to look first, in order:
src/ — the command class. Read how it detects existing phpunit.xml, resolves the test directory, and decides on namespace conventions. This is the whole package, and it is a compact worked example of scaffolding edge cases.composer.json in the plugin — the published pestphp/pest and php constraints. Assume they cap at Pest 1.x; verify before planning any upgrade.extra.pest.plugins / auto-discovery registration — this tells you what a Pest plugin actually looks like when you need to write a different one.First real use case: a greenfield project where you want tests/Pest.php and the standard directory layout without hand-writing them, or a legacy PHPUnit repository where you are doing an incremental file-by-file conversion and want the harness set up correctly once.
The command itself is trivial; the patterns that matter are how you surround it so it does not become permanent technical debt.
Fresh-project bootstrap (run once, commit, remove). The scaffolding is a starting point, not a runtime dependency. After the first commit, the plugin has done its job:
composer require --dev pestphp/pest pestphp/pest-plugin-init
php artisan pest:init
git add tests/ phpunit.xml composer.json
git commit -m "chore: bootstrap Pest test harness"
git rm tests/Pest.php 2>/dev/null; # only if you do NOT want to keep generated config
composer remove --dev pestphp/pest-plugin-init
git commit -m "chore: drop one-shot pest-plugin-init"
The generated files (tests/Pest.php, TestCase, phpunit.xml) stay and become your code, under your maintenance. That is the intended outcome.
Legacy PHPUnit → Pest migration (incremental, not big-bang). Use the init command once to lay down the harness, then convert file-by-file, leaving *Test.php PHPUnit classes running untouched alongside new Pest files. The suite stays green the whole way through:
# Phase 1: harness in place, everything still PHPUnit
php artisan pest:init && ./vendor/bin/pest
# Phase 2: new tests written in Pest, old ones still class-based
# tests/Feature/NewThingTest.php -> Pest functional style
# tests/Feature/LegacyTest.php -> still extends PHPUnit\Framework\TestCase
# Phase 3: convert opportunistically, never in a single PR
This is the strongest argument for reading the package: it is a scoping artifact for what an init/migration flow must touch — config detection, namespace resolution, test-path conventions, idempotent re-runs, partial-failure recovery.
Custom test paths and namespaces. The generated phpunit.xml and Pest.php bake in tests/Feature, tests/Unit, and the Tests\ namespace. If your project uses non-standard paths, the scaffolding is still the right starting point; edit the generated files immediately after init rather than fighting the command:
<!-- phpunit.xml after init — adjust these before committing -->
<testsuites>
<testsuite name="Feature">
<directory>app/Domain/*/Tests/Feature</directory>
</testsuite>
</testsuites>
Composer-managed integration (what a plugin looks like). Read this as the template for writing your own console command plugin: a service provider registered via extra.pest.plugins, a command class extending the Pest/PHPUnit console command base, and config-file publishing. It is a 20-minute source of edge cases for any init/scaffold/codemod command you build internally.
Idempotent re-runs. The command is designed to tolerate partially-initialized projects — re-running after a partial failure should not clobber hand-edited files. If you adopt the pattern rather than the package, replicate this: detect existing config, merge rather than overwrite, and fail loudly only when the state is genuinely ambiguous.
The removal-to-adoption inversion — use core --init instead. On Pest 2.x/3.x, treat this package as a removal project, not an adoption decision:
composer remove --dev pestphp/pest-plugin-init
./vendor/bin/pest --init # core equivalent
# then delete extra.pest.plugins and allow-plugins entries from composer.json
./vendor/bin/pest # prove the suite still passes
One dev-only PR per affected repo, ~30–60 minutes each.
./vendor/bin/pest --init. If it appears in a lockfile anywhere, that is a removal task, not an upgrade task.require-dev on a new project. You will get a dependency that does nothing core --init doesn't and will block your next Pest major. The opportunity score is an artifact of zero adoption on an archived repo, not evidence of an unmet niche — do not burn a spike rediscovering this.allow-plugins. Plugin registration now requires an explicit allow-plugins entry. A missing entry silently disables the plugin and the command "does not exist" — a confusing failure for a package that isn't actually broken. Check composer config allow-plugins before assuming the install failed.composer why-not pestphp/pest before planning a Pest upgrade. If this package is the blocker, that is the trigger to remove it — forking is only justified where a repo provably cannot reach core --init, and should require a named owner and written justification.phpunit.xml and composer.json in place. On a legacy tree, inspect that it did not clobber custom testsuites, bootstrap paths, or autoload-dev entries before committing.phpunit.xml. If a run failed halfway, prefer reverting to a clean tree and re-running over re-running on the broken state.pestphp/pest-plugin-init reappears in any lockfile prevents the slow reintroduction of a dependency nobody should have added. Publish a one-line standing decision — remove on sight, do not adopt, do not fork absent a named legacy blocker — so the question is answered once rather than spiked per repository.--init.How can I help you explore Laravel packages today?