pestphp/pest-plugin-rector
The official Pest plugin for Rector that automates test code migrations and upgrades. Convert PHPUnit tests to Pest, upgrade Pest 1 to 2/3, and apply custom Rector rules through a single, easy-to-configure ruleset—keeping your test suite clean and modern.
Install as a dev-only dependency alongside Rector — this is a codemod toolchain, never a runtime library:
composer require --dev pestphp/pest-plugin-rector rector/rector
Pin all three (pestphp/pest-plugin-rector, rector/rector, pestphp/pest) to exact versions in the same commit. There is no service provider, no facade, no publishable config — consumption happens entirely through Rector's rector.php.
Look here first: vendor/pestphp/pest-plugin-rector/src/. The README is thin, so the Rector/ rule classes and Set/ set-list classes are the documentation. Each rule class name states the exact transformation it performs; read them before enabling anything. Confirm the exact FQCN of the set-list class and its constant names on disk — the plugin tracks Rector's config API and names shift between versions.
Wire a minimal rector.php scoped to one file so the first run has a tight blast radius:
<?php
use Rector\Config\RectorConfig;
use Pest\Rector\Set\PestSetList; // verify exact FQCN in vendor/
return RectorConfig::configure()
->withPaths([__DIR__ . '/tests/Feature/ExampleTest.php'])
->withSets([PestSetList::PHPUNIT_TO_PEST]); // verify exact constant
Preview before touching anything:
vendor/bin/rector process --dry-run
Canonical first use case: convert one Laravel PHPUnit Feature test to Pest's functional style — class FooTest extends TestCase with $this->assert*() calls becomes it('...', function () { ... }) with expect()->… chains. Nail the loop on one file (dry-run → read diff → apply → ./vendor/bin/pest) before widening withPaths().
Path-scoped, one-directory-per-PR migration. Never point the set at all of tests/ on day one. Add tests/Feature/Auth, commit, run the suite, expand next PR. A 40-file auto-generated diff will get rubber-stamped; a 6-file diff gets read. It also lets you bisect a bad transformation to a specific file.
Combine with Pest's own migration tooling, on separate branches. Pest's migrator handles phpunit.xml, bootstrap, and tests/Pest.php plumbing; Rector handles the mechanical AST rewrite of test bodies. Don't let both touch the same file in one commit — run Rector on a throwaway branch, run the Pest migrator on another, then reconcile by hand.
Order sets deliberately. If rector.php also loads PHP-version or PHPUnit-level sets, list the Pest set after them. Rector applies sets in declared order, so you want setUp() → beforeEach() and $this->assertSame() → expect()->toBe() attempted only after the PHPUnit rules have normalised the source into their canonical form.
Drop to individual rules when the set is too broad. If the full set mangles constructs you intend to restructure by hand (data providers with yield, @depends), skip withSets() and enable specific classes:
->withRules([
\Pest\Rector\Rector\SomeSpecificRector::class, // confirm in vendor/
])
Gate CI on drift, not just green. After upgrading Pest, a deprecation that invalidates a helper should surface in the same PR:
- name: Rector dry-run on Pest tests
run: vendor/bin/rector process tests --dry-run
--dry-run alone only reports. Add the flag/exit-code setting that fails when changes exist to make it a real gate, and pair it with a test-count and coverage assertion so a rule that silently deletes or weakens a test cannot pass.
Baseline before and after. Record ./vendor/bin/pest --coverage and the total assertion/test count on the PHPUnit branch. After each Rector pass, compare. If ExampleTest.php drops from 12 assertions to 9, something was lost — that diff needs manual review even though the suite is green.
Retire it post-migration. Once conversion is done, remove the dependency (or keep it pinned but disable the set if stray PHPUnit files remain). An unmaintained codemod left enabled in CI is a pure upgrade tripwire with zero remaining benefit.
It's a codemod, not an oracle — budget a manual tail. Mocks ($this->createMock, partials), @depends ordering, complex yield data providers, and any reliance on $this-> context inside a closure routinely fail to transform. On a real Laravel suite, expect roughly 30% manual cleanup; the plugin clears the boring 70%.
Silent semantic drift is the real risk. A green suite proves nothing about assertion strength. Assertions can be weakened, RefreshDatabase lifecycle reordered, tearDown semantics lost. Review every file's diff, even when tests pass — and especially when the diff is large enough that you're tempted not to.
withPaths() rewrites whatever the glob matches. Point it at explicit directories (tests/Feature, tests/Unit), never the repo root, and never app/. Add ->withSkip() for fixture files, snapshot directories, or anything that only looks like a test.
Verify idempotency before trusting it as a gate. Run vendor/bin/rector process tests --dry-run twice. The second run against an already-transformed tree must produce zero changes. If a rule keeps proposing edits, it isn't idempotent and will churn your CI gate forever — skip that rule class, file an issue, and clear Rector's cache (--clear-cache) whenever results look stale.
Don't let Rector and Pint fight in the same commit. Run Pint after Rector applies, as a separate commit, so formatting churn doesn't obscure the semantic diff you actually need to review.
Pin aggressively and validate on a scratch branch. With 16 stars and 0 dependents there is essentially no community pressure to keep the rule-set API aligned with Rector majors. Assume you are the person who finds the breakage.
Treat the source as the documentation and name an owner. No vendor SLA, thin issue history. MIT licensing makes forking fine — just put the fork path and an owner in your runbook before you need it.
Watch for framing ambiguity. The package description also frames it as a way to write Pest-style tests for your own custom Rector rules (fixtures plus before/after transform assertions). That is a materially different daily use than PHPUnit→Pest migration. Confirm early, by reading src/ and whatever test scaffolding ships, which capability you actually installed — the two use cases imply different rector.php wiring and different CI steps.
How can I help you explore Laravel packages today?