Migrating PHPUnit → Testo
SkillFiles & storageMigrate an existing PHPUnit (or Pest) test suite to Testo — via the Rector bridge, via AI-agent rewriting, or a combination. Use when the user says "migrate from PHPUnit", "port phpunit tests", "convert TestCase to Testo", or has files extending PHPUnit\Framework\TestCase that need to move to Testo's attribute-based style.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Migrating PHPUnit → Testo skill
What this skill tells your AI
The instructions your AI receives, as published by php-testo/testo in skills/testo-migrate-from-phpunit/SKILL.md and read by ahel’s review.
Testo is similar in spirit to PHPUnit but not source-compatible: assertion argument order flips, discovery is attribute-based, and there is no base class. This skill orchestrates the migration as phases, then routes to one of two detailed approach references:
- Approach A — Rector-assisted (
references/migrate-with-rector.md):testo/bridge-rectordoes the mechanical bulk deterministically; an AI/human pass finishes the structural part. Recommended when the bridge can be installed. - Approach B — AI-agent rewrite (
references/migrate-with-agents.md): subagents port each file end-to-end using the mapping. For when Rector is unavailable, the suite is small, or tests are too unusual for the rules.
The PHPUnit → Testo construct mapping (shared by both) lives in references/phpunit-to-testo-map.md.
Run every command from the project root (never cd into vendor/bin). <skillDir> is this skill's
own directory (the folder holding SKILL.md). Fetch https://php-testo.github.io/llms.txt before
writing tests.
Pest → Testo: the bridge ships only
expect()->toX()→Assert::*; thetest()/it()functional→class restructuring is not automatable. Treat a Pest suite as Approach B (AI rewrite) with that one Rector set as a helper. The phases below still apply.
Phase 1 — Restore point (BLOCKING — do this first)
Migrating tests is a breaking change to the test suite. Your very first action is to make sure there is a restore point, then tell the user — and WAIT for their acknowledgement before changing any test. A migration can leave the suite red; the user must be able to roll back cleanly.
- Check the working tree:
git status --short. - If it is not clean, tell the user and ask them to commit or stash first — do not migrate on top of unrelated uncommitted work (a rollback would also discard theirs).
- If it is clean, state the rollback plan explicitly, e.g. "I'll work on a branch
migrate-to-testo; if the migration fails you cangit checkout <previous-branch>to discard everything." Prefer a dedicated branch (git checkout -b migrate-to-testo) or, at minimum, confirm the current commit is a safe point to reset to. - Say plainly: the test suite may be red during and after migration; the restore point is how we undo it if it doesn't pan out.
If the project is not under version control, this is blocking: tell the user a migration without a restore point is unsafe and ask them to initialise git (or make a backup copy) before proceeding.
Do not continue to Phase 2 until the user has acknowledged the restore point.
Phase 2 — Choose the scope
Decide which tests migrate in this run. Offer the user these options (and pick a sensible default from the layout if they're indifferent):
- Unit tests only — the safe, common starting point: pure, mock-light tests port cleanly. Good first cutover.
- A named directory / suite — e.g.
tests/Unit/Domain, one bounded slice. - The whole test suite — everything at once. Heaviest; only for small suites or a committed cutover.
Migrate one slice at a time and keep the rest on PHPUnit until each slice is green — don't run both
runners against the same tests in CI. Carry the chosen scope (as directory paths) into every later
phase and into the --scope / --path flags of the scripts.
Phase 3 — Detect tooling & choose the approach
1. Resolve the PHP binary
Pin <php> to an absolute path so it's stable across the run and across subagents:
php -r "echo PHP_BINARY;" # e.g. C:\php\php.exe or /usr/bin/php
2. Run the pre-flight check
<php> <skillDir>/scripts/precheck.php --scope=<dir> # repeat --scope per in-scope dir
It reports whether testo/bridge-rector (and therefore Rector) is installed — testo/bridge-rector
alone is enough, it pulls Rector in — and surveys the test surface (files, ~tests, and the
hard-to-convert constructs per directory). Read its output before choosing.
3. Present the approach options and let the user choose
Based on the pre-flight result, offer:
- Rector-assisted (Approach A) — recommended when the bridge is available. Fast, deterministic
bulk conversion + a finishing AI pass. →
references/migrate-with-rector.md. - AI-agent rewrite (Approach B) — everything ported by subagents. Use when the bridge isn't
installable, the suite is tiny, or tests are too unusual for the rules. →
references/migrate-with-agents.md. - Combine — Rector-assisted for the mechanical-heavy directories, AI-only for the gnarly ones (heavy mocking, custom constraints). Run the two references per slice.
State the trade-off honestly: Rector ≠ a complete migration. It never removes extends TestCase
or reconciles discovery, so every approach ends with an AI/human structural pass and a full-suite
verification. The choice is mainly how much of the mechanical rewriting Rector does for you vs. the
AI doing it (where the AI must apply the argument-order flip by hand, more error-prone).
If precheck.php says RECTOR PATH: NOT READY, default to recommending the bridge install
(one composer require --dev testo/bridge-rector) unless the user declines — then use Approach B.
Phase 4 — Execute the chosen approach
Follow the matching reference verbatim — each has its own stages, gates, and troubleshooting:
| Choice | Reference |
|---|---|
| Rector-assisted | references/migrate-with-rector.md |
| AI-agent rewrite | references/migrate-with-agents.md |
| Combine | both, applied per directory slice |
Both routes use the same building blocks:
- Mapping:
references/phpunit-to-testo-map.md— the authoritative construct table + pitfalls. - Work-list script:
scripts/scan-residuals.php— ranks files and writes batched work-lists (<outDir>/migration-report.md+<outDir>/migration-batches/NNN.json). - Porting subagent:
references/subagent-port-prompt.md— the fixed, verbatim template for porting/finishing one file.
Subagent concurrency (important — differs from sibling skills)
Migration subagents each rewrite a different, independent test file and run only that file
(--path=<file>), so a batch may be dispatched in PARALLEL — one subagent per file in the batch,
in a single message, then wait for all verdicts before the next batch. This is the opposite of
testo-mutation-testing / testo-increase-coverage, where subagents share source/reports and must
run strictly sequentially. Keep two things out of the parallel subagents' hands: testo.php
(configured once by you, not per file) and any shared fake/helper two files need (create it once,
up front). Process one batch at a time, in order; read only the current batch.
When filling the subagent template, map the batch entry + your resolved paths to its placeholders:
| Placeholder | Source |
|---|---|
{{FILE}} | batch entry path |
{{NEEDS}} | batch entry needs[], rendered one item per line |
{{HINTS}} | batch entry hints{}, rendered key: hint per line |
{{MAP}} | absolute path to <skillDir>/references/phpunit-to-testo-map.md |
{{TEST_CMD}} | <php> vendor/bin/testo — just the binary; the template adds --json --path= itself |
{{TEST_DIR}} | the test directory the file lives under |
Phase 5 — Verify & retire the old harness
After the chosen approach's stages complete, run the shared final pass (detailed in each reference):
- Full in-scope run:
vendor/bin/testo --json --suite=<name>. Gate:status: "passed". Investigate everyfailures[]— the usual culprits are a flipped comparison, a class stillextends TestCase(undiscovered), or a provider method mistaken for a test. - Retire the old harness — only once green: delete the disposable Rector config (Approach A);
when the whole project is migrated, remove
phpunit.xml,phpunit/phpunitfromcomposer.json, and anytests/bootstrap.php. Optionally droptesto/bridge-rectorfromrequire-dev. - Report the before/after to the user: files migrated, tests now green under Testo, anything
left on PHPUnit, and any files a subagent reported
blocked(needs a human decision — usually heavy mocking or a PHPUnit-only constraint).
If a slice cannot be made green, fall back to the Phase-1 restore point and report to the user — do not leave a half-migrated, red suite in place.
Related skills
testo-configure— writingtesto.php(suites, finder, plugins).testo-write-tests—#[Test],Assert,Expect, lifecycle detail.testo-data-driven— choosing#[DataSet]/#[DataProvider]/#[DataZip]/#[DataCross].testo-coverage—#[Covers]policy and coverage wiring.
Signals
- GitHub stars
- 214
- Forks
- 17
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
testo-migrate-from-phpunit- Source
- github.com/php-testo/testo