Rector rules to convert test suites between Pest, PHPUnit and Testo.
Each direction ships a Rector set. Reference it with the typed handle from
Testo\Bridge\Rector\Set\TestoRectorSetList rather than the raw file path:
| Direction | Set constant | Status |
|---|---|---|
| Testo → PHPUnit | TestoRectorSetList::TESTO_TO_PHPUNIT |
Assert calls, Expect::exception (bare), throw SkipTest, #[Covers]→#[CoversClass], lifecycle attributes. |
| PHPUnit → Testo | TestoRectorSetList::PHPUNIT_TO_TESTO |
Assert calls (arg-order restored), expectException (bare), markTestSkipped, #[CoversClass]→#[Covers], lifecycle methods → attributes. |
| Pest → Testo | TestoRectorSetList::PEST_TO_TESTO |
expect()->toX() → Assert::* only. The functional→class restructuring (test()/it() → methods) is not automatable — see src/PestToTesto/TODO.md. |
The set files live under config/; the constants are absolute paths to those files, so they
also work with $rectorConfig->import(...).
Testo core ships no mocking, so test doubles convert through their own sets, one per target library.
Add one next to PHPUNIT_TO_TESTO, or run it on its own:
| Direction | Set constant | Target |
|---|---|---|
| PHPUnit → Double | TestoRectorSetList::PHPUNIT_TO_DOUBLE |
createMock/createStub and their expects/method/will*/with chains → \JMac\Testing\Double (testo/bridge-double). See src/PhpunitToDouble/TODO.md. |
| PHPUnit → Mockery | TestoRectorSetList::PHPUNIT_TO_MOCKERY |
The same chains → \Mockery::mock() + shouldReceive() (testo/bridge-mockery). See src/PhpunitToMockery/TODO.md. |
| Mockery → Double | TestoRectorSetList::MOCKERY_TO_DOUBLE |
mock/spy, shouldReceive/allows/expects, shouldHaveReceived, Mockery::close() → Double, after Double's migration table. See src/MockeryToDouble/TODO.md. |
return RectorConfig::configure()
->withPaths([__DIR__ . '/tests'])
->withSets([TestoRectorSetList::PHPUNIT_TO_TESTO, TestoRectorSetList::PHPUNIT_TO_DOUBLE]);Each mock rule rewrites a whole configuration statement or none of it. The rules do not track a double across statements, though: when one statement on a double is left for manual work while the double's factory and its other statements convert, the leftover one is what the finishing pass fixes.
Conversions that have no faithful counterpart in the target framework (constraints,
memory-leak / retry / repeat, Pest higher-order & arch() tests, etc.) are
not silently dropped: each is a documented stub rule plus an entry in the direction's
TODO.md.
Testo's comparison assertions are (actual, expected); PHPUnit's are (expected, actual).
Both AssertCall* rules swap the first two arguments accordingly — getting this wrong would
silently invert every comparison, so it is covered by fixtures.
Testo is self-hosted: the engine that discovers and runs tests is the same code Infection
mutates. A mutation on the run path (e.g. Sorter, PipeOptions) can break discovery itself,
producing spurious survivors/kills instead of a real mutation signal. Converting the unit-style
self-tests to PHPUnit lets Infection's PHPUnit adapter run them on a runner that shares no code
with the mutated engine — a mutation can then only be caught (or missed) by an assertion, never
by breaking the harness.
// rector.php
use Rector\Config\RectorConfig;
use Testo\Bridge\Rector\Set\TestoRectorSetList;
return RectorConfig::configure()
->withPaths([__DIR__ . '/tests'])
->withSets([TestoRectorSetList::TESTO_TO_PHPUNIT]);The rules are tested by Testo itself, with no PHPUnit dependency. A rule carries
#[\Testo\Bridge\Rector\Testing\TestRectorFixtures('<dir>')] pointing at co-located *.php.inc
fixtures (input + expected, separated by a ----- line; no separator = "must stay unchanged").
Each declared path is relative to the rule's own directory (or absolute) and must resolve within
the working directory — an escaping path is rejected; declaring the attribute with no paths tests
nothing. The reusable harness lives in src/Testing/ — attach RectorTestingPlugin to a suite whose
finder scans the rule sources, and each fixture is run through a freshly-booted Rector container
and reported as its own data set. Fixtures are export-ignored; the harness ships so downstream
rule authors can reuse it (testo/* are require-dev + suggest).
Rector runs in the test process, so the fixtures count toward code coverage like any test. Each fixture's coverage is scoped to the rule it exercises (the harness attaches a Testo\Codecov\CoverageScope), which keeps the harness itself and the rest of the run out of it. A rule that delegates to helpers of its own widens the scope with #[Covers] on the rule class — list the rule too, since a declared #[Covers] replaces the default:
#[TestRectorFixtures('MyRule')]
#[Covers(MyRule::class)]
#[Covers(MyHelper::class)]
final class MyRule extends AbstractRector { /* ... */ }#[CoversNothing] on the rule class keeps its fixtures out of coverage.