Assert
The plugin provides assertion functionality in tests through the Assert\Testo\Assert and Expect\Testo\Expect facades.
Plugin class: AssertPlugin\Testo\Assert\AssertPlugin. Included in SuitePlugins\Testo\Application\Config\Plugin\SuitePlugins — enabled by default.
Assert vs Expect
The difference between the facades is when the check happens:
- Assert
\Testo\Assert— assertions. Checked immediately, right on the same line: "check and forget". - Expect
\Testo\Expect— expectations. Registered during the test, but verified after the test finishes: "remember now, check later".
This separation removes naming dissonance. When you see Expect::exception()Expect::exception(string|\Throwable $classOrObject, bool $same = false): ExpectedExceptionExpects the test to throw the given exception. in a test, it's immediately clear that the check will happen later — after the test completes. While Assert::same()Assert::same(mixed $actual, mixed $expected, string $message = ''): voidStrict comparison of two values (===). fires right on that line.
Basic Assertions
Note that Testo uses a more intuitive argument order: $actual (the value being checked) comes first, then $expected (the expected value). This differs from the legacy xUnit approach.
For most checks, these methods are all you need:
Assert::same
Assert::same(mixed $actual, mixed $expected, string $message = ''): voidStrict comparison of two values (===).Assert::notSame
Assert::notSame(mixed $actual, mixed $expected, string $message = ''): voidChecks that two values are not identical (!==).Assert::equals
Assert::equals(mixed $actual, mixed $expected, string $message = ''): voidLoose comparison of two values (==).Assert::notEquals
Assert::notEquals(mixed $actual, mixed $expected, string $message = ''): voidChecks that two values are not equal (!=).Assert::true
Assert::true(mixed $actual, string $message = ''): voidChecks that the value is strictly true.Assert::false
Assert::false(mixed $actual, string $message = ''): voidChecks that the value is strictly false.Assert::contains
Assert::contains(iterable $haystack, mixed $needle, string $message = ''): voidChecks that the collection contains the given value.Assert::count
Assert::count(Countable|iterable $actual, int $expected, string $message = ''): voidChecks the number of elements in a collection.Assert::instanceOf
Assert::instanceOf(mixed $actual, string $expected, string $message = ''): ObjectTypeChecks that the object is an instance of the given class. Shortcut for Assert::object($obj)->instanceOf($class).Assert::blank
Assert::blank(mixed $actual, string $message = ''): voidChecks for absence of data.Unlike PHP's empty(), does not consider false, 0, and "0" as blank values, because they carry real data. Blank values are: null, empty string '', empty array [], and Countable objects with zero elements.
Assert::fail
Assert::fail(string $message = ''): neverForcefully fails the test.Useful for lines of code that execution should never reach.
$message -> Reason for the test failure.foreach ($users as $user) {
if ($user->isAdmin()) {
Assert::same($user->role, 'admin');
return;
}
}
Assert::fail('There should be at least one admin in the list');Custom Messages
Most methods accept an optional $message parameter. This is a custom description of what is being checked — it will appear in the report if the assertion fails. Works in both basic assertions (Assert::same()Assert::same(mixed $actual, mixed $expected, string $message = ''): voidStrict comparison of two values (===)., Assert::blank()Assert::blank(mixed $actual, string $message = ''): voidChecks for absence of data.) and assertion chains:
Assert::same($user->role, 'admin', 'User should have admin role');Assertion Chains
Instead of dozens of individual methods like assertStringContains(), assertArrayHasKey(), and twenty more with the string* prefix, Testo groups assertions into typed chains.
The idea is simple: the method at the start of the chain verifies that the value has the expected type, then opens access to type-specific checks. Methods can be called one after another:
Assert::string($email)->contains('@');
Assert::int($age)->greaterThan(0)->lessThan(150);
Assert::array($items)
->hasKeys('id', 'name')
->isList()
->notEmpty();
Assert::object($dto)->instanceOf(UserDto::class)->hasProperty('email');
Assert::iterable($collection)
->allOf('int')
->contains(42)
->hasCount(10);Assert::string
Checks that the value is a string and opens string-specific assertions.
Assert::string(mixed $actual): StringTypeExamples:
Assert::string($html)
->contains('<div>')
->notContains('<script>');StringType::contains
StringType::contains(string $needle, string $message = ''): staticChecks that the string contains the given substring.StringType::notContains
StringType::notContains(string $needle, string $message = ''): staticChecks that the string does not contain the given substring.Numeric Types
There are three entry points for numeric values. They only differ in the type check at the start — the chain methods are the same for all:
Assert::int
Assert::int(mixed $actual): IntTypeChecks that the value is an integer and opens numeric assertions.Assert::float
Assert::float(mixed $actual): FloatTypeChecks that the value is a floating-point number.Assert::numeric
Assert::numeric(mixed $actual): NumericTypeChecks that the value is numeric (int, float, or numeric string).Shared chain methods:
NumericType::greaterThan
NumericType::greaterThan(int|float $min, string $message = ''): staticChecks that the value is strictly greater than the given one.NumericType::greaterThanOrEqual
NumericType::greaterThanOrEqual(int|float $min, string $message = ''): staticChecks that the value is greater than or equal to the given one.NumericType::lessThan
NumericType::lessThan(int|float $max, string $message = ''): staticChecks that the value is strictly less than the given one.NumericType::lessThanOrEqual
NumericType::lessThanOrEqual(int|float $max, string $message = ''): staticChecks that the value is less than or equal to the given one.Assert::int(15)->greaterThan(10);
Assert::float(3.14)->lessThan(4.0);
Assert::numeric('42.5')->greaterThanOrEqual(0);Assert::iterable
Checks that the value is iterable and opens collection assertions.
Assert::iterable(mixed $actual): IterableTypeWorks with arrays and objects implementing Traversable.
If you pass a generator into the chain, it will be consumed — generators in PHP can only be iterated once.
Examples:
Assert::iterable($users)
->notEmpty()
->allOf(User::class)
->every(fn(User $u) => $u->isActive());IterableType::notEmpty
IterableType::notEmpty(string $message = ''): staticChecks that the collection contains at least one element.IterableType::contains
IterableType::contains(mixed $needle, string $message = ''): staticChecks that the collection contains the given value.IterableType::sameSizeAs
IterableType::sameSizeAs(iterable $expected, string $message = ''): staticChecks that the number of elements matches another collection.IterableType::hasCount
IterableType::hasCount(int $expected): staticChecks that the collection contains exactly the given number of elements.IterableType::allOf
IterableType::allOf(string $type, string $message = ''): staticChecks that all elements are of the given type (get_debug_type(): 'int', 'string', class name).IterableType::every
IterableType::every(callable $callback, string $message = ''): staticChecks that every element satisfies the given predicate.Assert::array
Checks that the value is an array and opens array-specific assertions.
Assert::array(mixed $actual): ArrayTypeInherits all methods from Assert::iterable()Assert::iterable(mixed $actual): IterableTypeChecks that the value is iterable and opens collection assertions. and adds array-specific checks.
Examples:
Assert::array($config)
->hasKeys('host', 'port')
->doesNotHaveKeys('password');
Assert::array([1, 2, 3])->isList()->allOf('int')->sameSizeAs([4, 5, 6]);ArrayType::hasKeys
ArrayType::hasKeys(int|string ...$keys): staticChecks that the array contains all listed keys.ArrayType::doesNotHaveKeys
ArrayType::doesNotHaveKeys(int|string ...$keys): staticChecks that the array does not contain any of the listed keys.ArrayType::isList
ArrayType::isList(string $message = ''): staticChecks that the array is a list (sequential integer keys starting from 0).Assert::object
Checks that the value is an object and opens object-specific assertions.
Assert::object(mixed $actual): ObjectTypeExamples:
Assert::object($event)
->instanceOf(OrderCreated::class)
->hasProperty('orderId');ObjectType::instanceOf
ObjectType::instanceOf(string $expected, string $message = ''): staticChecks that the object is an instance of the given class or interface.ObjectType::hasProperty
ObjectType::hasProperty(string $propertyName, string $message = ''): staticChecks that the object has the given property.Assert::json
Checks that the string contains valid JSON and opens structure assertions.
Assert::json(string $actual): JsonAbstractYou can determine the type of the JSON value at the start, after which type-specific checks become available:
JsonAbstract::isObject
JsonAbstract::isObject(): JsonObjectChecks that the JSON represents an object.JsonAbstract::isPrimitive
JsonAbstract::isPrimitive(): JsonCommonChecks that the JSON represents a primitive value (string, number, boolean, null).JsonAbstract::isStructure
JsonAbstract::isStructure(): JsonStructureChecks that the JSON represents a structure (object or array).JsonAbstract::maxDepth
JsonAbstract::maxDepth(int $expected): staticChecks that the JSON nesting depth does not exceed the given limit.JsonAbstract::empty
JsonAbstract::empty(): JsonCommonChecks that the JSON object or array is empty.JsonStructure::count
JsonStructure::count(int $count, string $message = ''): staticChecks the number of elements in a JSON array or object.JsonObject::hasKeys
JsonObject::hasKeys(array|string $keys, string $message = ''): JsonObjectChecks that the JSON object contains the given keys.JsonStructure::assertPath
JsonStructure::assertPath(string $path, callable $callback): staticChecks a nested value at the given path.The callback receives a JsonAbstract for the value at the specified path, allowing you to build nested assertion chains.
JsonCommon::matchesType
JsonCommon::matchesType(string $type): staticValidates the JSON structure against a Psalm type.Accepts an extended Psalm type annotation — for example, 'array{foo: bool, bar?: non-empty-string}' or 'list<array{id: positive-int}>'.
JsonCommon::matchesSchema
JsonCommon::matchesSchema(string $schema): staticValidates the JSON structure against a JSON Schema.// Type check and structure validation
Assert::json($string)->isObject()->hasKeys('id', 'name');
Assert::json($string)->isArray()->count(5);
// Check nested values by path
Assert::json($response->body())
->isObject()
->assertPath('data.users', fn(JsonAbstract $json) =>
$json->isArray()->count(10)
);
// Validate against a Psalm type
Assert::json('{"foo": true, "bar": "test"}')
->matchesType('array{foo: bool, bar?: non-empty-string}');
// Validate against a JSON Schema
Assert::json($string)->matchesSchema($schemaJson);
// Get the decoded value
$data = Assert::json($string)->isObject()->decode();Expectations (Expect)
Unlike assertions, expectations are registered during test execution and verified after the test finishes. This is useful when the result needs to be evaluated by a side effect — for example, a thrown exception or a memory state.
Expect::exception
Expects the test to throw the given exception.
Expect::exception(string|\Throwable $classOrObject, bool $same = false): ExpectedExceptionIf the test finishes without an exception or with a different one, it is considered failed. Instead of a class name you can pass a specimen object — that lets you express several expectations in a single line, without separate withMessage() and withCode() calls.
The behavior of $same depends on what you pass as the first argument.
In the default mode ($same = false) the comparison is fairly lenient:
- a class-string is matched via
instanceof, so the class itself and any of its subclasses will pass; - a specimen object adds message and code comparison to the
instanceofcheck; an empty message andcode === 0are treated as unspecified and skipped.
// only instanceof RuntimeException is checked because message and code are default (empty and 0)
Expect::exception(new RuntimeException());
// instanceof RuntimeException + message and code comparison
Expect::exception(new RuntimeException('failed', 42));In the strict mode ($same = true) each variant tightens up to its maximum:
- a class-string requires an exact class match, subclasses no longer pass;
- an object means the test must throw the very same instance (compared with
===).
Additional constraints can be chained via ExpectedException\Testo\Assert\Api\ExpectedException (withMessage, withCode, fromMethod, etc.).
Parameters:
$classOrObject- Class, interface, or specimen object of the expected exception.
$same- When
true, performs the strictest comparison available for the given input type.
Examples:
use Testo\Expect;
#[Test]
public function throwsOnInvalidInput(): void
{
// instanceof is enough — InvalidArgumentException or any subclass will pass
Expect::exception(\InvalidArgumentException::class);
$service->process(null);
}// Exact class match: subclasses of RuntimeException will fail the check
Expect::exception(\RuntimeException::class, same: true);// Specimen object: class, message, and code are all expressed at once
Expect::exception(new PaymentException('insufficient funds', 402));You can narrow down the expected exception using chain methods:
ExpectedException::fromMethod
ExpectedException::fromMethod(string $class, string $method): selfChecks that the specified method is present in the exception's call stack.Can be called multiple times — every specified method must be present in the call stack for the check to pass.
The call stack in an exception is captured at the moment of its creation, not when it is thrown. So this checks where the exception was created, not where it was rethrown via throw.
// Make sure the exception originated in validation,
// not rethrown from somewhere else
Expect::exception(ValidationException::class)
->fromMethod(UserValidator::class, 'validate');ExpectedException::withMessage
ExpectedException::withMessage(string $message): selfChecks the exact exception message.A subsequent call replaces the previous expectation.
ExpectedException::withMessagePattern
ExpectedException::withMessagePattern(string $pattern): selfChecks that the message matches a regular expression.A subsequent call replaces the previous expectation.
ExpectedException::withMessageContaining
ExpectedException::withMessageContaining(string $substring): selfChecks that the message contains the given substring.Can be called multiple times — the message must contain every specified substring.
ExpectedException::withCode
ExpectedException::withCode(int|array $code): selfChecks the exception code. You can pass a single value or an array of acceptable codes.A subsequent call replaces the previous expectation.
ExpectedException::withoutPrevious
ExpectedException::withoutPrevious(): selfChecks that the exception has no previous exception.ExpectedException::withPrevious
ExpectedException::withPrevious(string|\Throwable $classOrObject, ?callable $assertion = null): selfChecks for a previous exception of the given type.A subsequent call replaces the previous expectation.
$assertion -> Optional callback that receives an ExpectedException for the previous exception — this allows building nested checks with the same API: verify the message, code, or even its own withPrevious().Expect::exception(PaymentException::class)
->withPrevious(
GatewayException::class,
fn (ExpectedException $previous) => $previous
->withCode(503)
->withMessageContaining('connection refused'),
);All chain methods can be combined in any order, building a precise description of the expected exception:
Expect::exception(PaymentException::class)
->fromMethod(PaymentGateway::class, 'charge')
->withMessageContaining('insufficient funds')
->withCode([402, 422])
->withPrevious(GatewayException::class);Expect::notLeaks
Expects the objects to be released from memory after the test finishes.
Expect::notLeaks(object ...$objects): NotLeaksUseful when you need to make sure a service properly releases its resources.
Examples:
#[Test]
public function serviceReleasesResources(): void
{
$connection = new Connection();
$service = new Service($connection);
Expect::notLeaks($connection, $service);
$service->process();
// After the test, Testo will verify that $connection and $service are no longer held in memory
}Expect::leaks
Expects the objects to remain in memory after the test finishes.
Expect::leaks(object ...$objects): LeaksUseful for verifying that a cache or another mechanism actually retains objects.
PHP may not collect objects if the test finishes with a thrown exception. There are also known issues with garbage collection on macOS.
Examples:
#[Test]
public function cachePersistsObjects(): void
{
$entity = new User();
$cache->store($entity);
Expect::leaks($entity);
// After the test, Testo will verify that $entity is still held in memory (by the cache)
}