Skip to content
Back to skills

Testo Migrate From Phpunit

ASecurity

Migrate 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.

  • 222 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added August 31, 2026
testinggophpbashtestinggit

Works with

  • cli

Security analysis

A100/100

Pro scans all 8 files and shows the line behind each finding

Scanned October 1, 2026

npx -y skills add php-testo/testo --skill testo-migrate-from-phpunit --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Testo Migrate From Phpunit?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Testo Migrate From Phpunit
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/php-testo-testo-migrate-from-phpunit/badge)](https://www.skillsdirectory.com/skills/php-testo-testo-migrate-from-phpunit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: testo-migrate-from-phpunit
description: Migrate 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.
---

# Migrating PHPUnit → Testo

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-rector` does
  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`).

> Pest → Testo: the bridge ships only `expect()->toX()` → `Assert::*`; the `test()`/`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.

1. Check the working tree: `git status --short`.
2. 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).
3. 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 can `git 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. Check `git rev-parse --verify migrate-to-testo` first: a branch left by an
   earlier attempt sits on an old base. When it exists, ask the user whether to pick a new name, delete
   the old branch, or continue on it; before continuing, compare its merge base with the current head of
   the main branch and tell the user when main has moved ahead.
4. 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.

Record the baseline before touching any file: `<php> vendor/bin/phpunit --log-junit=<out>/phpunit-junit.xml`
over the same scope. Phase 5 compares the migrated run against it test by test.

## 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:

```bash
php -r "echo PHP_BINARY;"     # e.g. C:\php\php.exe or /usr/bin/php
```

### 2. Run the pre-flight check

```bash
<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.**

It also lists what `phpunit.xml(.dist)` sets that Testo would silently lose (`<php>` ini/env/const,
`bootstrap`, `<testsuites>`, groups, extensions, source and coverage scope), each with its Testo
counterpart. Keep the list: `testo.php` is written from it (a lost `intl.default_locale` fails tests
that passed under PHPUnit), with one `SuiteConfig` per `<testsuite>` and the `<php>` settings at the
top of the file. For an alternate config such as `phpunit-without-intl.xml`, re-run with
`--phpunit-config=<file>`.

Compare the project's minimum PHP (`require.php` in its `composer.json`) with the one `testo/testo`
requires (PHP 8.2; the constraint is in `vendor/testo/testo/composer.json`). When the project supports
older versions, ask the user how to handle them: raise the project's minimum, keep a PHPUnit job for
the old versions during the transition, or knowingly accept the gap. The CI matrix follows that answer.

### 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 detaches classes from `TestCase`
and marks their tests, but mocks and PHPUnit-only constructs are left over, 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):

1. **Full in-scope run:** `vendor/bin/testo --json --suite=<name>`. **Gate:** `status: "passed"`.
   Investigate every `failures[]` — the usual culprits are a flipped comparison, a class still
   `extends TestCase` (undiscovered), or a provider method mistaken for a test.
2. **Same tests:** run `<php> vendor/bin/testo --suite=<name> --log-junit=<out>/testo-junit.xml`, then
   `<php> <skillDir>/scripts/compare-junit.php <out>/phpunit-junit.xml <out>/testo-junit.xml`.
   **Gate:** exit 0 (`IDENTICAL`): every test method ran as many times as under PHPUnit. Matching
   totals are not enough, a dropped test can hide behind an extra one. Add `--strip-test-prefix` if
   methods lost their `test` prefix and `--map='Old\Ns\=New\Ns\x27` if tests moved namespace. An
   undiscovered test fails nothing, it just drops out. The assertion count is not comparable between
   the two runners; both JUnit reports carry it (`assertions` on each `<testcase>` and `<testsuite>`),
   so a large per-test gap is a lead to inspect, never a gate.
3. **Polish (needs `testo/bridge-rector`):** run the `testo-polish` Rector set over the scope as its own
   pass, per Stage A6 of `references/migrate-with-rector.md`, then repeat steps 1–2: `compare-junit.php`
   must stay `IDENTICAL`.
4. **Retire the old harness — only once green:** delete the disposable Rector configs (Approach A);
   when the whole project is migrated, remove `phpunit.xml`, `phpunit/phpunit` from `composer.json`,
   and any `tests/bootstrap.php`. Optionally drop `testo/bridge-rector` from `require-dev`. Classes
   carrying a `#[Skip]` that names a PHPUnit base from vendor come first: rewrite that base for Testo
   and remove the attributes. Remove `phpunit/phpunit` only after that, since without it the vendor
   base no longer loads and test discovery fails.
   **CI:** add a job that runs `vendor/bin/testo` per suite (with the PHP extensions each suite
   needs) next to the existing one, on the PHP versions agreed in Phase 3 step 2. When the project
   calls a shared reusable workflow that runs PHPUnit, ask the user before touching the call: replace
   it with a job of its own, keep it and add a Testo job next to it, or pass the Testo command to the
   shared workflow if it takes one. The shared workflow itself stays as it is.
5. **Report** the before/after to the user: files migrated, tests now green under Testo, anything
   left on PHPUnit (list each explicitly: `phpunit.xml*` files, `phpunit/phpunit` and PHPUnit add-ons
   such as watchers in `composer.json`, composer scripts calling `phpunit`, CI jobs, an
   `infection.json*` still set up for PHPUnit, bases from `vendor/` that pull PHPUnit in
   transitively), 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` — writing `testo.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.

Files in this skill

  • SKILL.md9.1 KB
  • references/migrate-with-agents.md6.2 KB
  • references/migrate-with-rector.md7.5 KB
  • references/phpunit-to-testo-map.md8.8 KB
  • references/subagent-port-prompt.md5.7 KB
  • scripts/precheck.php8.9 KB
  • scripts/scaffold-rector-config.php5.6 KB
  • scripts/scan-residuals.php8.7 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.