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
toBe(mixed $expected)passes when the subject and expected value are identical with===.toEqual(mixed $expected)passes when the values are deeply equal.toEqualCanonicalizing(mixed $expected)passes when the values are deeply equal after the matcher recursively ignores list order in array values.toBeOneOf(mixed ...$options)passes when the subject is identical to one option.toBeIn(iterable $haystack)passes when the subject is identical to an item in the iterable.
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
toBeInstanceOf(string $class)requires an instance of the given class or interface.toBeTrue()requirestrue.toBeFalse()requiresfalse.toBeNull()requiresnull.toBeArray()requires an array.toBeString()requires a string.toBeInt()requires an integer.toBeFloat()requires a float.toBeBool()requires a boolean.toBeCallable()requires a callable.toBeIterable()requires an iterable.
Strings and collections
toContain(mixed $needle)checks that a string contains a string, or that an iterable contains an identical value.toHaveCount(int $count)checks that a countable or traversable subject has the given count.toBeEmpty()checks that a string, countable, or traversable subject contains nothing.toHaveLength(int $length)checks the code-point count of a valid UTF-8 string. If UTF-8 validation fails, it checks the byte count. It usescount()for an array orCountablesubject.toHaveKey(int|string $key)checks that an array orArrayAccesssubject contains the key.toContainSubset(array $subset)checks that an array contains every subset key. Nested arrays are also partial subsets, so the related subject arrays can contain extra keys. Non-array values use thetoEqual()rules.toMatch(string $pattern)checks that a string matches the regular expression.toStartWith(string $prefix)checks that a string starts with the prefix.toEndWith(string $suffix)checks that a string ends with the suffix.
Matchers that consume a Traversable do not rewind it after consumption.
Numbers
Numeric matchers accept integer or float subjects:
toBeGreaterThan(int|float $bound)toBeGreaterThanOrEqual(int|float $bound)toBeLessThan(int|float $bound)toBeLessThanOrEqual(int|float $bound)toBeWithin(float $delta, float $of)
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.