Move from PHPUnit
Greenlight and PHPUnit use different test structures. A migration usually changes the test support code, but much test logic can remain the same. A bundled Rector rule automates the mechanical part of the change. This guide describes the concepts behind the conversion.
Convert tests automatically
Greenlight includes the Greenlight\Rector\PhpUnitToGreenlightRector rule for
Rector 2. Install Rector as a development dependency:
composer require --dev rector/rector:^2.5
The rule rewrites final, attribute-based PHPUnit 10+ test classes. It converts
the TestCase parent, hooks, attributes, assertions, expectException()
blocks, markTestSkipped(), and fail().
Register the rule in a rector.php file that selects your test directories:
<?php
declare(strict_types=1);
use Greenlight\Rector\PhpUnitToGreenlightRector;
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([__DIR__ . '/tests'])
->withImportNames(removeUnusedImports: true)
->withRules([PhpUnitToGreenlightRector::class]);
The rule converts a final class only when each member has a faithful Greenlight equivalent. All other classes remain valid PHPUnit code, so a suite can move in steps. Converted classes run with Greenlight. The remaining classes continue to run with PHPUnit.
A class does not convert when it uses:
- a non-final test class
- test doubles such as
createMock(), which you convert manually to strict doubles #[Depends],setUpBeforeClass(),tearDownAfterClass(), or traits- assertions without a Greenlight matcher, for example file or XML assertions
assertEmpty()orassertNotEmpty(), because Greenlight uses different empty-value rules- multiple data providers or multiple requirements on one declaration
#[RunClassInSeparateProcess]or#[PreserveGlobalState]- other inherited
TestCaseAPI that the rule cannot prove safe
A custom failure message on an assertion has no Greenlight equivalent. By default, a message prevents the conversion of the class. Use this configuration to remove the messages:
->withConfiguredRule(PhpUnitToGreenlightRector::class, [
PhpUnitToGreenlightRector::DROP_ASSERTION_MESSAGES => true,
])
Two conversions change the code shape. An expectException() block becomes a
toThrow() expectation over an arrow function, and the earlier statements do
not move. expectExceptionMessage() finds a substring, so the rule writes a
quoted matching: pattern and not an exact message: constraint.
The rule preserves each repeated #[TestWith] row. It converts
#[RunInSeparateProcess] and #[RunTestsInSeparateProcesses] to #[Isolated].
It rejects class-process and global-state options that have different
Greenlight behavior.
The rule removes coverage metadata attributes, for example #[CoversClass],
because coverage configuration belongs in greenlight.php. Rector’s printer
also reflows each converted class. Run your code-style fixer after the
conversion. Then run the suite one time with --workers=1 before you enable
parallel workers.
Map the concepts
- Replace
extends TestCasewith a plain class. - Add
#[Test]to each public test method. - Replace
setUp()with#[Before]on a public method. - Replace
tearDown()with#[After]on a public method. - Replace
#[DataProvider('cases')]with#[DataSet('cases')]. - Keep the static data provider on the test class.
- Replace
#[TestWith([1, 2])]with#[DataRow([1, 2])]. - Add an optional label to
#[DataRow]. - Replace group annotations with the repeatable
#[Group('slow')]attribute. - Add
#[Group('slow')]to a method or class. - Replace
$this->markTestSkipped($reason)withthrow new SkipTest($reason). - Replace requirement attributes with a
#[SkipUnless]condition. - Use
#[SkipUnless(ExtensionLoaded::class, 'redis')]for an extension check. - Replace
$this->assert...()calls with staticExpect::that(...)chains. - Replace
createMock()andgetMockBuilder()with the injectedDoublesservice. - Use the
mock(),stub(), andspy()methods to create doubles. - Replace
setUpBeforeClass()static code with per-class harness services. - Replace
#[RunInSeparateProcess]with#[Isolated]. - Replace data flow from
#[Depends]with explicit fixture or harness state. - Remove
#[Depends]after the test no longer accepts the producer result. - Keep
@codeCoverageIgnoreand related annotations. - Use
#[CoverageIgnore]as the native equivalent.
Convert assertions
Expectations start with Expect::that(). They do not use methods on the test
class.
// PHPUnit // Greenlight
$this->assertSame('a', $value); Expect::that($value)->toBe('a');
$this->assertEquals($expected, $order); Expect::that($order)->toEqual($expected);
$this->fail('Reason'); Fail::because('Reason');
$this->assertTrue($open, 'Order must stay open'); Expect::that($open)->because('Order must stay open')->toBeTrue();
$this->assertInstanceOf(Response::class, $r); Expect::that($r)->toBeInstanceOf(Response::class);
$this->assertCount(3, $items); Expect::that($items)->toHaveCount(3);
$this->expectException(DomainException::class); Expect::that($fn)->toThrow(DomainException::class);
$this->assertGreaterThanOrEqual(3, $n); Expect::that($n)->toBeGreaterThanOrEqual(3);
$this->assertIsArray($value); Expect::that($value)->toBeArray();
$this->assertContains($needle, $haystack); Expect::that($haystack)->toContain($needle);
$this->assertEqualsCanonicalizing($a, $b); Expect::that($b)->toEqualCanonicalizing($a);
$this->assertJson($payload); Expect::that($payload)->toBeJson();
$this->assertJsonStringEqualsJsonString($e, $a); Expect::that($a)->toMatchJson($e);
Other type predicates include toBeString(), toBeInt(), toBeFloat(),
toBeBool(), toBeCallable(), and toBeIterable().
Membership matchers include toBeOneOf() and toBeIn(). Other matchers
include toHaveLength() and toContainSubset().
See Greenlight\Expect\Expectation for the complete list.
These differences are important:
toEqual()uses defined deep-equality rules.- Integers and floats compare by numeric value.
- Other scalar values compare strictly.
- Arrays compare by keys and recursively equal values.
- Objects compare by exact class and all properties, with private properties.
- Unlike types do not use loose equality. Thus,
'1'does not equal1. - PHPUnit
assertEmpty()uses PHPempty()semantics. - Greenlight
toBeEmpty()accepts strings, arrays,Countable, and iterables. - Convert
assertEmpty()andassertNotEmpty()manually. ->not()applies only to the next matcher.->because()replaces the PHPUnit$messageargument and applies only to the next matcher.toThrow()accepts a callable subject and an optional message constraint.- Use
message:for exact equality. - Use
matching:for a regular expression. - Do not use
message:andmatching:in one call.
- Use
Fail::because()replaces$this->fail()and supports explicit type guards.Fail::because()counts as an expectation and reports the guard location.- A failed matcher throws immediately. Greenlight has no soft-assertion mode.
Replace manual sleep() calls or retry loops with eventually():
Expect::eventually(fn() => $repository->find($id))
->within(2.0)
->toEqual($expected);
Use consistently()->for() when a value must not change. A probe exception
stops the poll unless retryOnException() lists its type.
Convert test doubles
Constructor injection supplies the Doubles service.
Greenlight does not create tolerant doubles. PHPUnit createMock() can create
a double whose methods return null or automatic stubs.
Greenlight has no equivalent object.
mock(Type::class, fn (MockPlan $plan) => ...) creates a strict mock.
Greenlight verifies each planned expectation at the end of the test.
An unplanned call fails the test immediately. Configure each return value with
andReturns(), andReturnsSequence(), andReturnsUsing(), or andThrows().
Replace willReturnOnConsecutiveCalls() with andReturnsSequence(...). The
sequence consumes one value for each call.
A call after the last value is a test-author error.
Replace willReturnCallback() with andReturnsUsing(fn (...) => ...). The
callback receives the call arguments.
Replace argument constraints with Greenlight\Doubles\Argument:
// PHPUnit // Greenlight
$mock->method('save')->with($this->anything()); $plan->expects('save')->with(Argument::any());
$this->isInstanceOf(Order::class) Argument::type(Order::class)
$this->callback(fn ($v) => $v > 0) Argument::predicate(fn ($v) => $v > 0, 'positive')
$this->equalTo($expected) Argument::equals($expected)
Use a captured argument instead of callback inspection:
$captor = $plan->expects('save')->once()->andReturns(true)->captureArgument(0);
// ... exercise the subject ...
Expect::that($captor->value())->toBeInstanceOf(Order::class);
stub(Type::class) supplies a collaborator and rejects each interaction.
If the collaborator must return a value, use a mock with explicit expectations.
spy(Type::class) records calls only for methods that return nothing. A spy
does not create a return value.
Read records with $this->doubles->callsTo($spy, 'method'). Check the records
with Expect.
$gateway = $this->doubles->mock(PaymentGateway::class, function (MockPlan $plan) use ($amount, $ok) {
$plan->expects('charge')->with($amount)->once()->andReturns($ok);
});
Greenlight verifies mocks when the per-test scope closes. You do not need a
Mockery::close() equivalent.
Greenlight can double interfaces and non-final classes. It rejects final classes, readonly classes, and enums.
The error recommends an interface. Greenlight does not support partial mocks or static method mocks.
After migration, strict doubles can expose interactions that old tests accepted.
See test doubles for the complete doubles API.
Replace class fixtures
Replace setUpBeforeClass() and static fixture properties with per-class
harness services.
A per-class harness service is a typed object with PerClass scope. Greenlight
creates one instance for each test class.
Greenlight injects this instance into each test constructor. It disposes the instance after the class completes.
Plugins register harness services. A plugin implements HarnessProvider and
returns service definitions with their scopes.
For shared suite fixtures, move the fixtures to a small plugin. Do not keep them in a static property on the test class.
Understand the deliberate differences
These differences are intentional:
- Greenlight has no
TestCasebase class. - Tests declare dependencies in the constructor.
- The runner supplies the declared dependencies.
- Greenlight has no inherited assertion API or
parent::setUp()chain. - Greenlight has no test method name pattern.
- The
#[Test]attribute identifies each test method. - Greenlight has no
#[Depends]. - Test dependencies create hidden order requirements that conflict with parallel execution.
- Put expensive shared state in a class-scoped or suite-scoped harness service.
- Tests run in parallel worker processes by default.
- Use
#[Isolated]for a test that must own its process. - External dependencies require an explicit parallel strategy.
- Use a channel for one resource for each worker.
- Use
#[RequiresResource]to limit access to a shared dependency. - Doubles are strict.
- Unplanned interactions fail, and Greenlight does not invent return values.
Migration sequence
- Add
greenlight.php. - Configure the test directories.
- Run the bundled Rector rule across the suite.
- Convert one remaining leaf test class manually.
- Remove the base class.
- Add
#[Test]to each test method. - Convert assertions to
Expect::that(). - Convert data providers.
- Keep the provider body when only the attribute must change.
- Convert mocks after the other test code.
- Use strict-double failures to find loose assumptions in the old tests.
- Run with
--workers=1to exclude parallel execution from the first runs. - Remove
--workers=1. - Correct failures that occur only with parallel workers.