Skip to content
...

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::null

Assert::null(mixed $actual, string $message = ''): voidChecks that the value is null.

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.
php
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:

php
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:

php
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): StringType

Examples:

php
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.
php
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): IterableType

Works 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:

php
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): ArrayType

Examples:

php
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): ObjectType

Examples:

php
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): JsonAbstract

You 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::isArray

JsonAbstract::isArray(): JsonArrayChecks that the JSON represents an array.

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.

JsonCommon::decode

JsonCommon::decode(): mixedReturns the decoded JSON value.
php
// 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): ExpectedException

If 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 instanceof check; an empty message and code === 0 are treated as unspecified and skipped.
php
// 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:

php
use Testo\Expect;

#[Test]
public function throwsOnInvalidInput(): void
{
    // instanceof is enough — InvalidArgumentException or any subclass will pass
    Expect::exception(\InvalidArgumentException::class);

    $service->process(null);
}
php
// Exact class match: subclasses of RuntimeException will fail the check
Expect::exception(\RuntimeException::class, same: true);
php
// 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.

php
// 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().
php
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:

php
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): NotLeaks

Useful when you need to make sure a service properly releases its resources.

Examples:

php
#[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): Leaks

Useful 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:

php
#[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)
}