Greenlight

Documentation

Use Greenlight

On this page

Expectations

Use Expect::value() to check a value. Use Expect::calling() to check a call. A value expectation applies one or more typed matchers to its subject:

use Greenlight\Expect\Expect;

Expect::value($order->status())->toBe(OrderStatus::Paid);

A matcher throws immediately if it does not pass. Greenlight reports the source location. It also reports expected and actual values when the matcher supplies them.

Function syntax

You can use Greenlight\expect() instead of the Expect class for value and call expectations:

use function Greenlight\expect;

expect('paid')->toBe('paid');
expect()->calling(static fn(): int => 2 + 2)->toReturn(4);

The runner loads the function before configuration and test discovery. It is available in test-file declarations, data providers, test methods, worker processes, and watch runs. Composer autoload alone does not load it.

Outside the runner, load the helper explicitly after the Composer autoloader:

require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/vendor/greenlight/greenlight/src/Expect/functions.php';

expect($value) has the same generic return type and matchers as Expect::value($value). It never executes a callable subject. This rule also applies to callable strings, arrays, and objects. expect(null) checks a null value.

With no argument, expect() returns an ExpectationBuilder. Use its calling($callback) method for call assertions and temporal probes. This method has the same behavior as Expect::calling($callback). It does not execute the call.

Editors can index the helper source and its generic PHPDoc. The PHPStan extension registers that source for static analysis. Without the extension, add the file to PHPStan’s scanFiles:

parameters:
    scanFiles:
        - vendor/greenlight/greenlight/src/Expect/functions.php

The generic conditional return type distinguishes no argument from explicit null and other known value types. For broad subject types such as mixed or object, PHPStan needs the extension to distinguish a value from the internal default-argument marker. Without the extension, use Expect::value() for those subjects.

Calls and return values

calling() accepts a callable with no required arguments. It does not execute the callable until the first matcher. toReturn() compares the return value with ===:

Expect::calling(fn() => $calculator->total())->toReturn(19.99);

Use returnValue() for other value matchers:

Expect::calling(fn() => $repository->find($id))
    ->returnValue()
    ->toBeInstanceOf(Order::class);

returnValue() does not execute the callable. Immediate matchers in the same call chain share one captured outcome. The outcome preserves the return value or the exact throwable object. An unexpected throwable propagates from a return-value matcher.

value() never invokes a callable subject. Use it to check callback identity or the callable type. Value expectations do not expose toThrow(). Call expectations expose toThrow(), toReturn(), and returnValue(). Native PHP declarations keep these method sets separate.

Matcher chains

Matchers in a chain use the same subject:

Expect::value($response->body())
    ->toBeString()
    ->toMatchJson('{"accepted":true}');

Start a separate expectation for each subject.

not() negates the next matcher only:

Expect::value($errors)
    ->not()->toBeEmpty()
    ->toHaveCount(1);

because() adds a reason to all subsequent matchers in the chain. Another because() call replaces the reason:

Expect::value($responseBody)
    ->because('successful payments require payment and receipt references')
    ->toHaveKey('payment_id')
    ->toHaveKey('receipt_url')
    ->because('payment responses must not expose card details')
    ->not()->toHaveKey('card_number')
    ->not()->toHaveKey('cvv');

The first reason applies to both required keys. The second reason applies to both excluded keys. If a matcher fails, its failure message includes the current reason after the word because.

Greenlight applies PHP’s trim() to the reason. The result must not be empty. The reason also applies after returnValue() and after a temporal matcher returns an immediate expectation. It does not apply to a separate expectation created inside a callback. Use a new expect() chain for assertions that do not need the reason. not() still negates only the next matcher.

Matcher reference

Identity and equality

toEqual() compares integers and floats by numeric value. It compares other scalars strictly. It compares arrays by key and recursively equal values. Objects must have the same class and recursively equal properties. This rule includes private properties.

Object comparisons distinguish shared objects from equal copies. They also require equal cycle shapes.

Both equality matchers report unsupported cyclic array traversal with InvalidArgumentException. Compare selected acyclic values instead. Object cycles and shared references to acyclic arrays remain supported.

Enum cases compare by identity. DateTimeInterface values compare by instant at microsecond precision.

Canonicalizing equality does not inspect object properties. Thus, a list in an object property keeps its order.

Type predicates

Strings and collections

Matchers that consume a Traversable do not rewind it after consumption.

Numbers

Numeric matchers accept integer or float subjects:

toBeWithin() passes when the absolute difference between the subject and $of is no greater than $delta. Use a finite tolerance of zero or more.

JSON

toBeJson() requires a string that contains valid JSON.

toMatchJson(string $expected) decodes both strings and compares their structures with toEqual() semantics. JSON object key order does not matter.

Exceptions

toThrow() checks the throwable from a call:

Expect::calling(fn() => $service->load('missing'))
    ->toThrow(NotFound::class);

With no argument, toThrow() accepts any Throwable. Use not()->toThrow() to require a call that does not throw:

Expect::calling($callback)->not()->toThrow();

not()->toThrow(NotFound::class) also passes when the call throws a different type. A constrained negative checks the constraint, not the absence of all throwables.

Pass a Throwable instance to require the callable to throw that exact object:

$failure = new DomainException('Order is closed.');

Expect::calling(fn() => throw $failure)
    ->toThrow($failure);

Constrain the message by exact value or regular expression:

Expect::calling($callback)->toThrow(
    DomainException::class,
    message: 'Order is closed.',
);

Expect::calling($callback)->toThrow(
    DomainException::class,
    matching: '/closed/i',
);

message: and matching: are mutually exclusive. Use them only with a throwable class-string. A Throwable instance already specifies one exact object.

A typed callback can specify the throwable type and check the caught throwable. Greenlight runs it only after the throwable type matches:

Expect::calling(fn() => $fixtureManager->start())
    ->toThrow(
        static function (IntegrationFixtureError $error): void {
            Expect::value($error->getPrevious())
                ->toBeInstanceOf(LengthException::class);
        },
    );

Declare one named, non-null Throwable parameter type by value. Return void from the callback. The parameter type specifies the expected throwable class.

The callback can contain ordinary expectations. A failed callback expectation keeps its diagnostic. An eventually() expectation retries this failure.

With not(), a failed callback expectation means that the throwable does not match. Thus, the negated toThrow() expectation passes. Other throwables from the callback stop the matcher. An instance constraint matches only the exact object. A different instance passes the negated expectation.

An eventually() expectation retries when the callable throws a different instance. It passes when the callable throws the specified object.

Asynchronous state

eventually() repeats a call until its matcher passes or within() expires. The first matcher starts the calls:

Expect::calling(fn() => $repository->find($id))->returnValue()->eventually()
    ->pollEvery(0.100)
    ->within(5.0)
    ->toEqual($expected);

The default poll interval is 25 ms. A probe exception stops the polls unless retryOnException() lists its type:

Expect::calling(fn() => $client->fetch($id))->returnValue()->eventually()
    ->retryOnException(NotFoundYet::class)
    ->within(2.0)
    ->toBeInstanceOf(Response::class);

retryOnException() accepts Exception subclasses, but not Error types.

consistently() requires the first probe result to match, then checks for the full duration:

Expect::calling(fn() => $outbox->messagesFor($id))->returnValue()->consistently()
    ->pollEvery(0.050)
    ->for(0.5)
    ->toHaveCount(1);

For exception checks, put the time controls directly after calling():

Expect::calling(fn() => $client->fetch($id))
    ->eventually()
    ->within(2.0)
    ->toThrow(Gone::class);

Each poll executes the call once. Exception matchers inspect the captured throwable and do not require retryOnException(). Direct temporal call chains also accept toReturn() for strict return-value equality.

A successful temporal matcher retains the final outcome. Further matchers in that chain check this outcome without another call or poll. For example, toBeArray()->toHaveKey('id') polls only until toBeArray() passes.

For both temporal chains, pollEvery() accepts a finite duration of at least 0.001 seconds. within() and for() accept finite durations greater than zero.

Each temporal matcher counts as one expectation. Temporal matchers check the test-attempt deadline between probe calls. These checks cannot interrupt a blocked probe. With process-pool execution, the orchestrator can stop the worker after the timeout grace period. In-process execution has no such protection. See timeouts.

Explicit failures

If a test reaches an invalid state that does not fit a matcher, use Fail::because():

use Greenlight\Expect\Fail;

if (!$response instanceof SuccessResponse) {
    Fail::because(\sprintf(
        'Expected SuccessResponse, got %s.',
        \get_debug_type($response),
    ));
}

The call counts as an expectation and reports itself as the failure location. If the IDE cannot determine the type, use a manual guard.