rector/rector-phpunit
Rector rules for PHPUnit that automate test suite upgrades. Modernize assertions, replace deprecated APIs, and keep tests compatible across PHPUnit versions through automated, configurable refactoring—no manual edits required.
Architecture fit. rector/rector-phpunit is not runtime code — it is a ruleset plugin for Rector (the AST-based PHP refactoring engine) that automates PHPUnit major-version migrations. Architecturally this is a dev-tooling dependency with zero production footprint: it lives in require-dev, never ships to the app, and cannot affect request handling, container bindings, or Laravel bootstrapping. It plugs in as a config entry in rector.php (e.g. PHPUnitLevelSetList::UP_TO_PHPUNIT_110) and operates only on the paths you scope. For a Laravel codebase this fits cleanly because Laravel's test layer is conventional PHPUnit — standard TestCase subclasses, standard assertions — which is exactly the surface these rules target.
Integration feasibility. High and low-cost. The package is first-party within the rectorphp GitHub org, versioned in lockstep with Rector releases, and is the ruleset Rector's own documentation points to. Installation is a single composer require --dev plus a config block; the first useful output (a diff) is available within minutes via --dry-run. The real feasibility question is not "can we install it" but "how much of our suite is idiomatic enough for the rules to cover."
Technical risk. Contained. Every change lands in version control as a reviewable diff, is reversible with git checkout, and is validated by the test suite itself — the rare tooling category where the artifact under change is also its own verification. The genuine risks are subtler:
assertRegExp → assertMatchesRegularExpression, setUp(): void, @dataProvider → #[DataProvider]), but custom constraints, mocked removed methods, and bespoke base-class helpers will be left untouched and must be fixed by hand.Key questions.
Stack fit. Native Composer/vendor/bin fit with no framework coupling. Configuration is a single rector.php at repo root; we scope withPaths() to tests/ plus any test-support directories to prevent Rector from touching application code. Runtime is developer machines and CI only — no effect on deploy artifacts, image size, or boot time.
Migration path. Branch-based, staged, reversible:
composer.json first, install, and let the suite break. This defines the true blast radius.vendor/bin/rector process tests/ --dry-run to size the diff and eyeball rule coverage before committing to anything.tests/Unit first (usually highest rule coverage, lowest custom-helper density), then tests/Feature, then shared test scaffolding.Compatibility. Pin rector/rector and rector/rector-phpunit to matching majors and commit both to composer.lock. Rector has meaningful BC breaks between its own majors, so the Rector upgrade cadence — not the PHPUnit ruleset — is the recurring compatibility cost. Check that no first-party package in the dependency tree pins PHPUnit below the target version, and confirm PHP version satisfies both the new PHPUnit and the chosen Rector major.
Sequencing. The dependency order is fixed: lock/baseline → PHPUnit bump → Rector dry-run → batched application → manual tail → full-suite verification → merge. CI enforcement (Rector in --dry-run/check mode on PRs) should be a later, optional phase, not part of the initial migration — adding a guard before the codebase is clean will just produce noise.
Maintenance. The package is actively maintained by the Rector core team and gains rulesets as PHPUnit ships majors — we inherit that work for free. Our own maintenance burden is version pinning plus a config edit per upgrade cycle, measured in minutes. The larger, less obvious cost is Rector itself: Rector majors introduce breaking config and rule changes, so adopting this package effectively enrolls us in Rector's upgrade cadence whether or not we ever migrate PHPUnit again.
Support. MIT, community-driven, GitHub issues only, no vendor SLA. The practical escalation path for hard cases is a paid migration service (Laravel Shift or a consultant) rather than package support. Rector's documentation is genuinely good, which materially lowers the support burden.
Scaling. CPU- and memory-intensive on large suites — Rector parses the full AST of every in-scope file. Mitigations are straightforward: scope to tests/, use Rector's parallel processing, and run it in CI rather than on developer machines. Because it is dev-only, there is no production scaling consideration whatsoever.
Failure modes. Ranked by likelihood and severity:
| Failure | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Rule silently no-ops due to version skew | Medium | High (false confidence) | Pin matched majors; review diff volume against expectation |
| Assertion rewritten with changed semantics | Low–Medium | High (tests pass but test less) | Mandatory human diff review |
| Partial transform leaves broken test file | Medium | Low (CI catches) | Batched application + suite run per batch |
| Memory exhaustion on large suite | Medium | Low | Scope paths, run in CI, chunk by directory |
| Pest tests skipped without warning | Medium if Pest present | Medium | Inventory Pest files separately; handle manually |
| Rector touches non-test code | Low | Medium | Explicit withPaths() scoping |
Ramp-up. Low. Engineers unfamiliar with Rector need roughly half a day to read the config, run a dry-run, and interpret the diff confidently; those who have used Rector need essentially none. The migration itself is a one-to-two-day effort for a mid-size Laravel application, dominated by manual remediation of the tail rather than by tool operation. Budget more if Pest is in play or if the suite leans heavily on custom test helpers.
Bottom line for the decision. Low-risk, low-cost, reversible dev-tooling with a clear one-time payoff and a modest recurring cadence cost inherited from Rector. The adoption signals are thin in absolute terms, but first-party org status substitutes for them. The decision hinges on whether a PHPUnit major upgrade is on the roadmap — if it is, this is straightforwardly worth piloting on a branch and measuring rule coverage before committing.
How can I help you explore Laravel packages today?