Browse documentation

Expectations

A Greenlight expectation starts with a subject value. It applies one or more typed matchers to that value:

use Greenlight\Expect\Expect;

Expect::that($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.

Matcher chains

and() starts a new chain from another value:

Expect::that($response->status())->toBe(200)
    ->and($response->body())->toMatchJson('{"accepted":true}');

not() negates the next matcher only:

Expect::that($result)->not()->toBeNull()
    ->and($errors)->toBeEmpty();

because() adds a reason to the next matcher only:

Expect::that($order->isOpen())
    ->because('a refund requires an open order')
    ->toBeTrue();

When the matcher fails, the failure message puts the reason after the word because:

Expected false to be true because a refund requires an open order.

The reason must not be empty. Temporal expectation chains also accept because().

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.

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

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.

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() invokes a callable subject and checks the throwable type:

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

Constrain the message by exact value or regular expression:

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

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

message: and matching: are mutually exclusive.

Asynchronous state

Expect::eventually() calls a probe immediately. It then polls until its matcher passes or within() expires:

Expect::eventually(fn() => $repository->find($id))
    ->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::eventually(fn() => $client->fetch($id))
    ->retryOnException(NotFoundYet::class)
    ->within(2.0)
    ->toBeInstanceOf(Response::class);

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

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

Each temporal matcher counts as one expectation. The test timeout limits its duration. The worker timeout remains the hard limit for a blocked probe.

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.