Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions bridge/rector/FEATURE_PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,31 @@ Conversion coverage across the three directions supported by `testo/bridge-recto
| **Data providers** (`#[DataProvider]`/`#[DataSet]` ↔ `->with`) | ✅ *`DataProviderToPhpUnitRector` renames `#[\Testo\Data\DataProvider]` → `#[DataProvider]` and `#[\Testo\Data\DataSet([…], 'label')]` → `#[TestWith([…], 'label')]` (both repeatable, args verbatim)* | ✅ *both `@dataProvider` annotation **and** `#[DataProvider]` attribute → `#[\Testo\Data\DataProvider]`; cross-class external form left as TODO* | 🟡 *inline `->with([ rows ])` → one repeated `#[\Testo\Data\DataSet]` per row; a named `->with('x')` / `dataset()` definition needs a provider — TODO* |
| **Groups** (`#[Group]`) | ✅ *`GroupToPhpUnitRector` expands variadic → repeated `#[Group]`; `GroupInheritanceToPhpUnitRector` flattens both the class-level inheritance union (parents + traits) and the method-level prototype chain (a leaf method inherits the groups of the same-named parent-class method). Residual: traits are intentionally not consulted at method level — matches Testo, whose prototype walk skips them* | ✅ *`GroupToTestoRector` collapses `@group` annotations **and** repeated `#[Group]` into one variadic `#[\Testo\Filter\Group]`* | ✅ *`->group('a','b')` → `#[\Testo\Filter\Group('a','b')]`* |
| **ExpectNoAssertions** (`#[\Testo\Assert\ExpectNoAssertions]` ↔ `#[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions]`) | ✅ *`ExpectNoAssertionsToPhpUnitRector` (attribute rename; both sides method/function-level only — no fan-out)* | ✅ *`DoesNotPerformAssertionsToTestoRector` (attribute rename)* | ➖ |
| **Mocks** (`createMock`/`createStub` + `expects`/`method`/`will*`/`with`) | ➖ | 🟡 *`CreateMockToDoubleRector` converts onto the Double bridge (`testo/bridge-double`): `createMock`/`createStub` → `Double::for`, `create{Mock,Stub}ForIntersectionOfInterfaces([A, B])` → `Double::for(A, B)`, and the configuration chain onto `expects`/`allows`/`with`/`returns`/`throws`/`resolves` — the invocation matcher moves onto the verb (`once`→`times(1)`, `exactly`→`times`, `never`→`never`, `atLeastOnce`/`atLeast`/`atMost`→`times(minimum:/maximum:)`, `any`→`allows`), the method name off `->method()` onto `expects('m')`, `withAnyParameters()` drops away, the returns (incl. `willReturnArgument($n)`→`resolves(fn (...$a) => $a[$n])`, `willReturnSelf()`→`returns(<the double>)`, and legacy `will(...)` incl. `onConsecutiveCalls`/`returnArgument`/`returnSelf`), the builder chain `getMockBuilder(X)->disableOriginalConstructor()->getMock()`→`Double::for(X)`, and `with()` constraints onto `Argument::*` (`anything`→`any`, `identicalTo`→`same`, `isInstanceOf`/`isType`→`type`, `callback`→`satisfies`, `contains`→`contains`, `matchesRegularExpression`→`matches`; `equalTo($x)`→bare `$x`, `isNull`/`isTrue`/`isFalse`→`null`/`true`/`false`; `greaterThan`/`lessThan`/`…OrEqual`, `isEmpty`, `stringContains`, `stringStartsWith`/`EndsWith`, `arrayHasKey`→`satisfies(fn ($value) => …)`; `logicalNot`→`Argument::not(...)`, `logicalOr`→`Argument::any(...)`). All-or-nothing per chain: `willReturnMap`, a variable matcher, a builder step beyond `disableOriginalConstructor` (or the bare constructor-calling `getMockBuilder(X)->getMock()`), `prophesize`, and `with()` constraints with no faithful form (`logicalAnd`, `equalToWithDelta`/`equalToCanonicalizing`, case-insensitive `stringContains`) leave the statement untouched — see `MockToTestoRector` (stub) and TODO.md* | ➖ |
| **Mocks** (`createMock`/`createStub` + `expects`/`method`/`will*`/`with`) | ➖ | ➖ *not part of this set: Testo core ships no mocking, so doubles convert through the mock sets below (Double or Mockery)* | ➖ |
| **Memory-leak expectations** | ⛔ *no PHPUnit equivalent* | ➖ | ➖ |
| **Retry / Repeat** (`#[Retry]`/`#[Repeat]`) | 🟡 *`RepeatRetryRector` converts `#[\Testo\Repeat]`/`#[\Testo\Retry]` → PHPUnit `#[Repeat]`/`#[Retry]` (PHPUnit 13.3+): `maxFailures`→`failureThreshold` (+1), Testo defaults made explicit. PHPUnit's are `TARGET_METHOD` only, so a class-level Testo attribute is fanned out onto each test method (a method's own attribute overrides it, not doubled); `markFlaky` is dropped (no PHPUnit equivalent)* | 🟡 *`RepeatRetryToTestoRector` converts `#[Repeat]`/`#[Retry]` → Testo's attributes: `failureThreshold`→`maxFailures` (−1; the default 1 folds to Testo's default 0 and is omitted)* | ➖ |
| **Fiber** (`#[RunInFiber]`, `Coroutine::spawn/await/concurrently`) | ⛔ *no PHPUnit/Pest equivalent — neither has a fiber/coroutine test attribute or an in-test coroutine scope* | ➖ | ➖ |
| **HTML report** (`HtmlPlugin`, `--log-html`) | ⛔ *not test code — a reporter configured in `testo.php` or by a flag, with nothing in a test file to convert* | ➖ | ➖ |
| **`uses()`** (Pest) | ➖ | ➖ | ⛔ *a converted function has no base class, traits or `$this` to attach to; closures that capture `$this`-shared state are left untouched* |
| **`arch()` tests** (Pest) | ➖ | ➖ | ⛔ *Testo has no arch-assertion subsystem* |

## Mock sets

Test doubles convert through their own sets, one per target library (`PHPUNIT_TO_DOUBLE`,
`PHPUNIT_TO_MOCKERY`, `MOCKERY_TO_DOUBLE`). Each rule rewrites a configuration statement whole or
leaves it untouched; the per-folder `TODO.md` lists the residuals.

| Feature / concept | PHPUnit → Double | PHPUnit → Mockery | Mockery → Double |
|---|:---:|:---:|:---:|
| **Creation** | 🟡 *`CreateMockToDoubleRector`: `createMock`/`createStub`/intersections/`disableOriginalConstructor()` builder → `Double::for`, `disableAutoReturnValueGeneration` → `->strict()`; `createConfiguredMock` → `Double::for` + `allows()->returns()` per entry; `createPartialMock`/`onlyMethods` → `->passthru()` (+ `passthru(new X(...))` when the constructor runs) + `allows()` per doubled method. Left: `prophesize`, `getMockForAbstractClass`, `addMethods`, the bare constructor-calling builder, strict partials* | 🟡 *`CreateMockToMockeryRector`: the same forms → `\Mockery::mock(...)->shouldIgnoreMissing()`, configured → quick definitions, partial → `\Mockery::mock('X[a]', $ctorArgs)`, all-real partial → `->makePartial()`. Same residuals* | 🟡 *`MockeryToDoubleRector`: `mock` → `->strict()`, `spy`/`shouldIgnoreMissing` → loose, `makePartial` → `->passthru()` (with ctor args → `passthru(new X(...))`), `'A, B'`/several targets → intersection, typed computed targets, `mock($object)` → proxied passthru, quick definitions and inline `->getMock()` split into statements. Left: bare `mock()`, full mocks with ctor args, `alias:`/`overload:`/`X[m]`* |
| **Call counts** | ✅ *matcher → `times()`/`never()`, `any` → `allows`* | ✅ *matcher → `once`/`times`/`never`/`atLeast()`/`atMost()`* | ✅ *`once` (the `expects` default), `twice`/`times`, `never`, `atLeast()`/`atMost()`, `between`, `zeroOrMoreTimes`, `shouldNotReceive`; `ordered()` kept, `globally()`/groups left manual* |
| **Returns** | ✅ *`willReturn*`, `willThrowException`, `willReturnCallback`/`Argument`/`Self`/`Map`, legacy `will(...)`; `willReturnReference` left manual* | ✅ *same set → `andReturn*`/`andThrow`/`andReturnUsing(<map lookup>)`* | ✅ *`andReturn(s)`/`Values` (literal or array-typed)/`Null`/`True`/`False`/`Self`/`Arg`/`Using`, `andThrow(s)` (object or class-string), `andThrowExceptions`; `andReturnUndefined`/`andSet`/`passthru` left manual* |
| **Argument matching** | ✅ *dedicated `Argument::*` matchers, every other constraint (incl. `logicalAnd`/`Xor`, delta/case/canonicalizing equality, `countOf`, `isJson`, `containsOnly*`, PHPUnit 12 type factories) → `satisfies()` over its PHP expression, verdicts pinned against PHPUnit in `PhpunitConstraintTest`. Left: computed case flag / type, `matches()` formats* | ✅ *dedicated Mockery matchers, the rest → `Mockery::on()` over the same expressions* | 🟡 *`with`/`withArgs` (literal, array-typed, closure)/`withNoArgs`/`withAnyArgs`/`withSomeOfArgs`, magic `allows()->m($x)`, Mockery matchers → `Argument::*` per the migration table; `hasKey`/multi-`contains`/`subset`/`ducktype` → `satisfies()`. Left: nested matchers, Hamcrest. Residual by design: Mockery compares loosely, Double strictly* |
| **Several expectations in one call** | ➖ | ➖ | ✅ *`shouldReceive('a', 'b')`, `shouldReceive(['a' => 1])`, `allows(['a' => 1])` → one statement per method (link arguments must be side-effect free)* |
| **Default expectations** (`byDefault`) | ➖ | ➖ | 🟡 *dropped when the expectation has no arguments and no count (Double tries the newest expectation first); otherwise left manual* |
| **Spy verification** | ➖ | ➖ | ✅ *`shouldHaveReceived`/`shouldNotHaveReceived` (plain, array-args, closure and magic forms) → `received()`/`->never()`, `shouldHaveBeenCalled`/`shouldNotHaveBeenCalled` → `received('__invoke')`; `Mockery::close()` → `Double::verifyAll()`* |
| **PHPUnit integration** | ➖ | ➖ | ✅ *`MockeryIntegrationToDoubleRector`: `MockeryPHPUnitIntegration` → `VerifiesDoubles`, `extends MockeryTestCase` → `extends TestCase` + the trait* |

## Remaining work (🧩 — actually tractable)

Pest → Testo opened up once the direction stopped chasing a *class* and targeted **free functions**
Expand Down Expand Up @@ -105,13 +122,7 @@ a `use (...)`-capturing closure, or any unrecognised modifier — see `src/PestT
The remaining ⛔ rows are intentionally out of scope: a missing target feature (`arch()`,
memory-leak, PHPUnit `assertThat` constraints), the substring-vs-regex
exception-message mismatch, or Pest `uses()` (a function has no base class / traits / `$this`).
Mocks moved off this list: with the Double bridge there is now a target API, so `createMock`/
`createStub` (and intersection mocks), their `expects`/`method`/`will*` chains, and `with()`
constraints convert as a documented 🟡 (`CreateMockToDoubleRector`), including intersection stubs,
`getMockBuilder(X)->disableOriginalConstructor()->getMock()`, `willReturnSelf()`, `withAnyParameters()`,
and the comparison/string/`logicalNot`/`logicalOr` constraints (via `satisfies`/`not`/`any`); only the
unmappable links (`willReturnMap`, a builder step beyond `disableOriginalConstructor`, `prophesize`,
`logicalAnd`, delta/canonicalizing equality) stay manual.
Mocks moved off this list: they convert through the dedicated mock sets (see "Mock sets" above).
Retry/Repeat moved off this list: PHPUnit 13.3 added `#[Repeat]`/`#[Retry]`, so both directions now
convert as a documented 🟡 (`RepeatRetryRector` / `RepeatRetryToTestoRector`).
PHPUnit's `markTestIncomplete` moved off this list — it now converts to a Skipped throw with an
Expand Down
25 changes: 23 additions & 2 deletions bridge/rector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,29 @@ Each direction ships a Rector set. Reference it with the typed handle from
The set files live under `config/`; the constants are absolute paths to those files, so they
also work with `$rectorConfig->import(...)`.

Conversions that have no faithful counterpart in the target framework (PHPUnit mocks,
constraints, memory-leak / retry / repeat, Pest higher-order & `arch()` tests, etc.) are
### Mock sets

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](https://testdoublephp.com/migrating-from-mockery). See `src/MockeryToDouble/TODO.md`. |

```php
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`.

Expand Down
23 changes: 23 additions & 0 deletions bridge/rector/config/mockery-to-double.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Testo\Bridge\Rector\MockeryToDouble\MockeryIntegrationToDoubleRector;
use Testo\Bridge\Rector\MockeryToDouble\MockeryToDoubleRector;

/**
* Mockery -> Double conversion set.
*
* Moves Mockery doubles — `mock()`/`spy()`, `shouldReceive()`/`allows()`/`expects()` chains,
* `shouldHaveReceived()` verification and `Mockery::close()` — onto Double (`testo/bridge-double`),
* following Double's Mockery migration table. Independent of the test framework, so it runs on PHPUnit
* and Testo suites alike. The forms with no faithful Double target are left in place — see
* bridge/rector/src/MockeryToDouble/TODO.md.
*/
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(MockeryToDoubleRector::class);

# A PHPUnit class verifying through Mockery's integration verifies through Double's instead.
$rectorConfig->rule(MockeryIntegrationToDoubleRector::class);
};
18 changes: 18 additions & 0 deletions bridge/rector/config/phpunit-to-double.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Testo\Bridge\Rector\PhpunitToDouble\CreateMockToDoubleRector;

/**
* PHPUnit mocks -> Double conversion set.
*
* Moves PHPUnit's `createMock()`/`createStub()` doubles and their `expects()/method()/will*()/with()`
* chains onto Double (`testo/bridge-double`). Pair it with `phpunit-to-testo.php` when migrating a
* suite, or run it alone. The forms with no faithful Double target are left in place — see
* bridge/rector/src/PhpunitToDouble/TODO.md.
*/
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(CreateMockToDoubleRector::class);
};
18 changes: 18 additions & 0 deletions bridge/rector/config/phpunit-to-mockery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Testo\Bridge\Rector\PhpunitToMockery\CreateMockToMockeryRector;

/**
* PHPUnit mocks -> Mockery conversion set.
*
* Moves PHPUnit's `createMock()`/`createStub()` doubles and their `expects()/method()/will*()/with()`
* chains onto Mockery, verified after every test by `testo/bridge-mockery`. Pair it with
* `phpunit-to-testo.php` when migrating a suite, or run it alone. The forms with no faithful Mockery
* target are left in place — see bridge/rector/src/PhpunitToMockery/TODO.md.
*/
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(CreateMockToMockeryRector::class);
};
6 changes: 2 additions & 4 deletions bridge/rector/config/phpunit-to-testo.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use Rector\Config\RectorConfig;
use Testo\Bridge\Rector\PhpunitToTesto\AssertCallToTestoRector;
use Testo\Bridge\Rector\PhpunitToTesto\CoversClassToCoversRector;
use Testo\Bridge\Rector\PhpunitToTesto\CreateMockToDoubleRector;
use Testo\Bridge\Rector\PhpunitToTesto\DataProviderAnnotationToTestoRector;
use Testo\Bridge\Rector\PhpunitToTesto\DataProviderAttributeToTestoRector;
use Testo\Bridge\Rector\PhpunitToTesto\DoesNotPerformAssertionsToTestoRector;
Expand Down Expand Up @@ -67,7 +66,6 @@
# Repeat/Retry method attributes (PHPUnit 13.3+) map onto Testo's #[Repeat]/#[Retry].
$rectorConfig->rule(RepeatRetryToTestoRector::class);

# Mocks/stubs onto the Double bridge: createMock/createStub → Double::for, and the
# expects()/method()/will*() configuration chain onto expects/allows/with/returns/throws/resolves.
$rectorConfig->rule(CreateMockToDoubleRector::class);
# Mocks are not converted here: Testo core ships no mocking, so the target library is a choice.
# Add `phpunit-to-double.php` or `phpunit-to-mockery.php` next to this set.
};
Loading
Loading