Browse documentation

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

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:

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:

Migration sequence

  1. Add greenlight.php.
  2. Configure the test directories.
  3. Run the bundled Rector rule across the suite.
  4. Convert one remaining leaf test class manually.
  5. Remove the base class.
  6. Add #[Test] to each test method.
  7. Convert assertions to Expect::that().
  8. Convert data providers.
  9. Keep the provider body when only the attribute must change.
  10. Convert mocks after the other test code.
  11. Use strict-double failures to find loose assumptions in the old tests.
  12. Run with --workers=1 to exclude parallel execution from the first runs.
  13. Remove --workers=1.
  14. Correct failures that occur only with parallel workers.