Greenlight

Documentation

Use Greenlight

On this page

Test doubles

Greenlight provides strict mocks, inert stubs, and spies that record calls. The per-test Doubles service supplies these doubles. Request this service through constructor injection:

use Greenlight\Attribute\Test;
use Greenlight\Doubles\Doubles;
use Greenlight\Doubles\MockPlan;
use Greenlight\Expect\Expect;

final class CheckoutServiceTest
{
    public function __construct(private Doubles $doubles) {}

    #[Test]
    public function chargesTheOrder(): void
    {
        $gateway = $this->doubles->mock(
            PaymentGateway::class,
            function (MockPlan $plan): void {
                $plan->expects('charge')
                    ->with(1999, 'GBP')
                    ->once()
                    ->andReturns('payment-123');
            },
        );

        $payment = new CheckoutService($gateway)->checkout(1999, 'GBP');

        Expect::value($payment->id)->toBe('payment-123');
    }
}

Greenlight verifies mocks when the test ends.

Incorrect use of the doubles API throws InvalidDoubleUsage. This exception identifies incorrect test code. It is not an expectation failure.

Double selection

These spy calls return null. A call to a spy method with any other native return type fails the test. PHPDoc return tags do not change this rule.

Mock call plans

The plan passed to mock() declares expectations with expects():

$plan->expects('reserve')
    ->with($sku, 2)
    ->once()
    ->andReturns(true);

Each method expectation has a default cardinality of at least one call. Use one of these methods to change its cardinality:

A call that does not match an expectation fails immediately. At teardown, an unmet expectation fails the test. Greenlight reports all unmet expectations together.

Greenlight examines expectations in declaration order. It uses the first unsaturated expectation that accepts the call.

Without an argument constraint, an expectation accepts each argument list that the method declaration permits. Greenlight rejects arguments that the method does not declare.

Mock responses

Each mock method with a native non-void return type needs an explicit response. A method declared void or without a native return type needs no response and returns null. PHPDoc return tags do not change this rule.

The following examples show separate response plans.

Return one value for every matched call:

$plan->expects('nextId')->andReturns('id-1');

Return successive values for two calls:

$plan->expects('nextId')
    ->times(2)
    ->andReturnsSequence('id-1', 'id-2');

Calculate the response from the call arguments:

$plan->expects('convert')
    ->andReturnsUsing(fn (int $value): int => $value * 2);

Throw an exception:

$plan->expects('load')
    ->andThrows(new NotFound('Missing record.'));

andReturnsSequence() consumes one value for each call that matches. Greenlight reports an error if a call occurs after the sequence is empty.

For a method that declares never, use andThrows(). If a configured answer returns, Greenlight reports an InvalidDoubleUsage error.

Argument matches

Bare values passed to with() use strict comparison (===):

$plan->expects('save')->with($expectedOrder);

Use Argument::equals($expectedOrder) to apply deep equality to a value object.

Use withNoArguments() to require a call that supplies no arguments:

$plan->expects('loadDefaults')->withNoArguments();

This constraint is useful when a method has optional or variadic parameters. with() requires at least one value or argument matcher.

Use Argument matchers for broader constraints:

use Greenlight\Doubles\Argument;

$plan->expects('save')->with(
    Argument::type(Order::class),
    Argument::predicate(
        fn (int $attempt): bool => $attempt > 0,
        'a positive attempt',
    ),
    Argument::any(),
);

Available matchers are:

Use Argument::intersection() when a value must have every specified type. Use Argument::union() when a value can have one or more specified types:

use Greenlight\Doubles\Argument;

$plan->expects('save')->with(
    Argument::intersection(Entity::class, Persistable::class),
    Argument::union('string', Stringable::class),
);

The bundled PHPStan extension preserves the combined value type in each ArgumentMatcher generic type. It reports a matcher in with() when its type cannot match the selected method parameter. Other type names use mixed, as they do for Argument::type().

Use a typed predicate to apply a type constraint and a value constraint:

use Greenlight\Doubles\Argument;

$plan->expects('save')->with(Argument::predicate(
    fn (Order $order): bool => $order->isReady(),
    'a ready order',
));

The parameter type rejects an incompatible value before the predicate runs. Greenlight rejects the plan if this type cannot match the method parameter.

Use Argument::allOf() to apply two or more constraints to one argument. Greenlight checks the matchers from left to right. allOf() does not accept a captor.

Argument capture

Capture one argument from every matched call:

$captor = $plan->expects('save')
    ->times(2)
    ->andReturns(true)
    ->captureArgument(0);

// Exercise the subject.

Expect::value($captor->values())->toHaveCount(2);
Expect::value($captor->value())->toBeInstanceOf(Order::class);

values() returns each captured value. value() returns the last value. It fails if the captor did not capture a value.

If a plan must capture more than one argument, put an explicit Argument::captor() inside with() for each argument.

Spy calls

callsTo() returns argument lists in call order. It copies each top-level argument value at the start of the call. Later assignments to a reference parameter do not change earlier recordings. Objects keep their identity. Greenlight does not clone them.

$events = $this->doubles->spy(EventPublisher::class);

new CheckoutService($events)->checkout();

Expect::value(
    $this->doubles->callsTo($events, 'publish'),
)->toEqual([[new OrderPlaced('order-1')]]);

Clone calls

Greenlight intercepts a public, non-final __clone() method. It does not run the application implementation:

An intercepted clone is a separate object with the same expectations and call history as the original double. This also applies to a clone of a clone. callsTo() accepts each of these objects and returns the same call history. This history contains calls to other methods.

Call counts apply across all these objects. A clone does not reset a planned count. For example, expects('__clone')->once() permits one clone in total.

If a test clones a mock, add a __clone expectation to its plan. If the test clones a stub, use a mock with an explicit clone expectation. A final __clone() keeps its implementation and can run application code.

Supported types and limits

Double targets are interfaces or non-final, non-readonly classes.

A class double intercepts only overridable public instance methods.

Concrete final, static, and protected methods keep their original implementations. Calls through these methods can run application code.

Greenlight does not run the class constructor when it creates a double. Prefer an interface at the application boundary.

Greenlight suppresses a non-final destructor. It cannot suppress a final destructor, which can run application code.

Object parameter defaults

Methods can declare object defaults and objects inside nested arrays. Greenlight copies each new expression from the method’s source file into the proxy. It preserves constructor arguments, imported names, and literal values. It does not construct these default objects when it creates the double. PHP constructs them when a call omits the applicable argument.

The method must have a readable PHP source file. Methods declared through eval() cannot supply object default expressions. Declare methods on separate lines so Greenlight can identify the original declaration. A default cannot depend on a private constructor that the proxy cannot access. Dynamic constant access also requires a class without private constants. These cases produce InvalidDoubleUsage.

Define unqualified constants before you create the double. Greenlight resolves these constants in the original namespace, with PHP’s global fallback, when it generates the proxy. Private scalar and enum constants retain their values.