Skip to content
...

Assert

Плагин предоставляет функционал проверок в тестах через фасады Assert\Testo\Assert и Expect\Testo\Expect.

Класс плагина: AssertPlugin\Testo\Assert\AssertPlugin. Входит в SuitePlugins\Testo\Application\Config\Plugin\SuitePlugins по умолчанию.

Assert vs Expect

Разница между фасадами — в том, когда происходит проверка:

  • Assert\Testo\Assert — утверждения. Проверяются здесь и сейчас, на той же строке: «проверил и забыл».
  • Expect\Testo\Expect — ожидания. Регистрируются во время теста, а проверяются уже после его завершения: «запомнил, в конце проверил».

Такое разделение убирает диссонанс в именовании. Когда вы видите в тесте Expect::exception()Expect::exception(string|\Throwable $classOrObject, bool $same = false): ExpectedExceptionОжидает, что тест выбросит указанное исключение., сразу понятно, что проверка произойдёт позже — после того, как тест завершится. А Assert::same()Assert::same(mixed $actual, mixed $expected, string $message = ''): voidСтрогое сравнение двух значений (===). сработает прямо на этой строке.

Базовые утверждения

Обратите внимание, что в Testo выбран более интуитивный прямой порядок аргументов: сначала идёт $actual (проверяемое значение), затем $expected (ожидаемое значение). Это отличается от устаревшего подхода xUnit.

Для большинства проверок достаточно этих методов:

Assert::same

Assert::same(mixed $actual, mixed $expected, string $message = ''): voidСтрогое сравнение двух значений (===).

Assert::notSame

Assert::notSame(mixed $actual, mixed $expected, string $message = ''): voidПроверяет, что два значения не идентичны (!==).

Assert::equals

Assert::equals(mixed $actual, mixed $expected, string $message = ''): voidНестрогое сравнение двух значений (==).

Assert::notEquals

Assert::notEquals(mixed $actual, mixed $expected, string $message = ''): voidПроверяет, что два значения не равны (!=).

Assert::true

Assert::true(mixed $actual, string $message = ''): voidПроверяет, что значение строго равно true.

Assert::false

Assert::false(mixed $actual, string $message = ''): voidПроверяет, что значение строго равно false.

Assert::null

Assert::null(mixed $actual, string $message = ''): voidПроверяет, что значение равно null.

Assert::contains

Assert::contains(iterable $haystack, mixed $needle, string $message = ''): voidПроверяет, что коллекция содержит указанное значение.

Assert::count

Assert::count(Countable|iterable $actual, int $expected, string $message = ''): voidПроверяет количество элементов в коллекции.

Assert::instanceOf

Assert::instanceOf(mixed $actual, string $expected, string $message = ''): ObjectTypeПроверяет, что объект является экземпляром указанного класса. Сокращение для Assert::object($obj)->instanceOf($class).

Assert::blank

Assert::blank(mixed $actual, string $message = ''): voidПроверяет отсутствие данных.

В отличие от PHP-функции empty(), не считает false, 0 и "0" пустыми значениями, потому что они несут в себе реальные данные. Пустыми считаются: null, пустая строка '', пустой массив [] и Countable-объекты с нулевым количеством элементов.

Assert::fail

Assert::fail(string $message = ''): neverПринудительно фейлит тест.

Полезен для строк кода, до которых выполнение не должно доходить.

$message -> Причина провала теста.
php
foreach ($users as $user) {
    if ($user->isAdmin()) {
        Assert::same($user->role, 'admin');
        return;
    }
}
Assert::fail('В списке должен быть хотя бы один администратор');

Пользовательские сообщения

Большинство методов принимают необязательный параметр $message. Это произвольное описание того, что именно проверяется — оно отобразится в отчёте, если утверждение не пройдёт. Работает как в базовых утверждениях (Assert::same()Assert::same(mixed $actual, mixed $expected, string $message = ''): voidСтрогое сравнение двух значений (===)., Assert::blank()Assert::blank(mixed $actual, string $message = ''): voidПроверяет отсутствие данных.), так и в цепочках проверок:

php
Assert::same($user->role, 'admin', 'У пользователя должна быть роль admin');

Цепочки проверок

Вместо десятков отдельных методов вроде assertStringContains(), assertArrayHasKey() и ещё двадцати с префиксом string*, Testo группирует проверки в типизированные цепочки.

Идея простая: метод в начале цепочки проверяет, что значение имеет нужный тип, а затем открывает доступ к специфичным для этого типа проверкам. Методы можно вызывать друг за другом:

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

Проверяет, что значение является строкой, и открывает строковые проверки.

Assert::string(mixed $actual): StringType

Примеры:

php
Assert::string($html)
    ->contains('<div>')
    ->notContains('<script>');

StringType::contains

StringType::contains(string $needle, string $message = ''): staticПроверяет, что строка содержит указанную подстроку.

StringType::notContains

StringType::notContains(string $needle, string $message = ''): staticПроверяет, что строка не содержит указанную подстроку.

Числовые типы

Для числовых значений есть три точки входа. Они отличаются только проверкой типа на входе, а набор методов в цепочке у всех одинаковый:

Assert::int

Assert::int(mixed $actual): IntTypeПроверяет, что значение является целым числом, и открывает числовые проверки.

Assert::float

Assert::float(mixed $actual): FloatTypeПроверяет, что значение является числом с плавающей точкой.

Assert::numeric

Assert::numeric(mixed $actual): NumericTypeПроверяет, что значение числовое (int, float или числовая строка).

Общие методы в цепочке:

NumericType::greaterThan

NumericType::greaterThan(int|float $min, string $message = ''): staticПроверяет, что значение строго больше указанного.

NumericType::greaterThanOrEqual

NumericType::greaterThanOrEqual(int|float $min, string $message = ''): staticПроверяет, что значение больше или равно указанному.

NumericType::lessThan

NumericType::lessThan(int|float $max, string $message = ''): staticПроверяет, что значение строго меньше указанного.

NumericType::lessThanOrEqual

NumericType::lessThanOrEqual(int|float $max, string $message = ''): staticПроверяет, что значение меньше или равно указанному.
php
Assert::int(15)->greaterThan(10);
Assert::float(3.14)->lessThan(4.0);
Assert::numeric('42.5')->greaterThanOrEqual(0);

Assert::iterable

Проверяет, что значение является iterable, и открывает проверки для коллекций.

Assert::iterable(mixed $actual): IterableType

Работает с массивами и объектами, реализующими Traversable.

Если передать в цепочку генератор, он будет потрачен — генераторы в PHP можно итерировать только один раз.

Примеры:

php
Assert::iterable($users)
    ->notEmpty()
    ->allOf(User::class)
    ->every(fn(User $u) => $u->isActive());

IterableType::notEmpty

IterableType::notEmpty(string $message = ''): staticПроверяет, что коллекция содержит хотя бы один элемент.

IterableType::contains

IterableType::contains(mixed $needle, string $message = ''): staticПроверяет, что коллекция содержит указанное значение.

IterableType::sameSizeAs

IterableType::sameSizeAs(iterable $expected, string $message = ''): staticПроверяет, что количество элементов совпадает с другой коллекцией.

IterableType::hasCount

IterableType::hasCount(int $expected): staticПроверяет, что коллекция содержит ровно указанное количество элементов.

IterableType::allOf

IterableType::allOf(string $type, string $message = ''): staticПроверяет, что все элементы имеют указанный тип (get_debug_type(): 'int', 'string', имя класса).

IterableType::every

IterableType::every(callable $callback, string $message = ''): staticПроверяет, что каждый элемент удовлетворяет переданному предикату.

Assert::array

Проверяет, что значение является массивом, и открывает проверки для массивов.

Assert::array(mixed $actual): ArrayType

Примеры:

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): staticПроверяет, что в массиве есть все перечисленные ключи.

ArrayType::doesNotHaveKeys

ArrayType::doesNotHaveKeys(int|string ...$keys): staticПроверяет, что в массиве нет ни одного из перечисленных ключей.

ArrayType::isList

ArrayType::isList(string $message = ''): staticПроверяет, что массив является списком (последовательные целочисленные ключи от 0).

Assert::object

Проверяет, что значение является объектом, и открывает проверки для объектов.

Assert::object(mixed $actual): ObjectType

Примеры:

php
Assert::object($event)
    ->instanceOf(OrderCreated::class)
    ->hasProperty('orderId');

ObjectType::instanceOf

ObjectType::instanceOf(string $expected, string $message = ''): staticПроверяет, что объект является экземпляром указанного класса или интерфейса.

ObjectType::hasProperty

ObjectType::hasProperty(string $propertyName, string $message = ''): staticПроверяет, что объект имеет указанное свойство.

Assert::json

Проверяет, что строка содержит валидный JSON, и открывает проверки структуры.

Assert::json(string $actual): JsonAbstract

На входе можно определить тип JSON-значения, после чего доступны специфичные для типа проверки:

JsonAbstract::isObject

JsonAbstract::isObject(): JsonObjectПроверяет, что JSON представляет объект.

JsonAbstract::isArray

JsonAbstract::isArray(): JsonArrayПроверяет, что JSON представляет массив.

JsonAbstract::isPrimitive

JsonAbstract::isPrimitive(): JsonCommonПроверяет, что JSON представляет примитивное значение (строка, число, boolean, null).

JsonAbstract::isStructure

JsonAbstract::isStructure(): JsonStructureПроверяет, что JSON представляет структуру (объект или массив).

JsonAbstract::maxDepth

JsonAbstract::maxDepth(int $expected): staticПроверяет, что глубина вложенности JSON не превышает указанную.

JsonAbstract::empty

JsonAbstract::empty(): JsonCommonПроверяет, что JSON-объект или массив пуст.

JsonStructure::count

JsonStructure::count(int $count, string $message = ''): staticПроверяет количество элементов в JSON-массиве или объекте.

JsonObject::hasKeys

JsonObject::hasKeys(array|string $keys, string $message = ''): JsonObjectПроверяет, что JSON-объект содержит указанные ключи.

JsonStructure::assertPath

JsonStructure::assertPath(string $path, callable $callback): staticПроверяет вложенное значение по указанному пути.

Callback получает JsonAbstract для значения по указанному пути, что позволяет строить вложенные цепочки проверок.

JsonCommon::matchesType

JsonCommon::matchesType(string $type): staticВалидирует структуру JSON по Psalm-типу.

Принимает расширенную аннотацию типа Psalm — например, 'array{foo: bool, bar?: non-empty-string}' или 'list<array{id: positive-int}>'.

JsonCommon::matchesSchema

JsonCommon::matchesSchema(string $schema): staticВалидирует структуру JSON по JSON Schema.

JsonCommon::decode

JsonCommon::decode(): mixedВозвращает декодированное значение JSON.
php
// Определение типа и проверка структуры
Assert::json($string)->isObject()->hasKeys('id', 'name');
Assert::json($string)->isArray()->count(5);

// Проверка вложенных значений по пути
Assert::json($response->body())
    ->isObject()
    ->assertPath('data.users', fn(JsonAbstract $json) =>
        $json->isArray()->count(10)
    );

// Валидация по Psalm-типу
Assert::json('{"foo": true, "bar": "test"}')
    ->matchesType('array{foo: bool, bar?: non-empty-string}');

// Валидация по JSON Schema
Assert::json($string)->matchesSchema($schemaJson);

// Получение декодированного значения
$data = Assert::json($string)->isObject()->decode();

Ожидания (Expect)

В отличие от утверждений, ожидания регистрируются во время выполнения теста, а проверяются после его завершения. Это удобно для ситуаций, когда результат нужно оценить по побочному эффекту — например, по выброшенному исключению или по состоянию памяти.

Expect::exception

Ожидает, что тест выбросит указанное исключение.

Expect::exception(string|\Throwable $classOrObject, bool $same = false): ExpectedException

Если тест завершится без исключения или с другим исключением, он считается проваленным. Вместо имени класса можно передать готовый объект-образец — тогда одной строкой задаётся сразу несколько ожиданий, без отдельных вызовов withMessage() и withCode().

Поведение $same зависит от того, что передано первым аргументом.

В режиме по умолчанию ($same = false) проверка довольно мягкая:

  • для имени класса используется instanceof, поэтому подойдут и сам класс, и любые его наследники;
  • для объекта-образца к instanceof добавляется сравнение сообщения и кода; пустое сообщение и code === 0 считаются незаданными и не проверяются.
php
// проверяется только instanceof RuntimeException
Expect::exception(new RuntimeException());

// instanceof RuntimeException + сравнение сообщения и кода
Expect::exception(new RuntimeException('failed', 42));

В строгом режиме ($same = true) каждая из проверок ужесточается до максимума:

  • для имени класса требуется точное совпадение, наследники уже не пройдут;
  • для объекта в тесте должен быть выброшен ровно тот же самый экземпляр (сравнение через ===).

Дополнительные ограничения можно навешивать через цепочку методов ExpectedException\Testo\Assert\Api\ExpectedException (withMessage, withCode, fromMethod и так далее).

Параметры:

$classOrObject
Класс, интерфейс или объект-образец ожидаемого исключения.
$same
Если true, применяется самая строгая проверка, доступная для переданного типа.

Примеры:

php
use Testo\Expect;

#[Test]
public function throwsOnInvalidInput(): void
{
    // Достаточно instanceof — подойдёт InvalidArgumentException и любой его наследник
    Expect::exception(\InvalidArgumentException::class);

    $service->process(null);
}
php
// Точное совпадение класса: наследники RuntimeException провалят проверку
Expect::exception(\RuntimeException::class, same: true);
php
// Объект-образец: разом задаём ожидаемый класс, сообщение и код
Expect::exception(new PaymentException('insufficient funds', 402));

С помощью цепочки методов можно уточнить, какое именно исключение ожидается:

ExpectedException::fromMethod

ExpectedException::fromMethod(string $class, string $method): selfПроверяет, что указанный метод присутствует в стеке вызовов исключения.

Метод можно вызывать несколько раз — для прохождения проверки в стеке должны присутствовать все указанные методы.

Стек вызовов в исключении заполняется в момент его создания, а не выброса. Таким образом, мы проверяем именно место создания, а не проброс через throw.

php
// Убеждаемся, что исключение возникло именно в валидации,
// а не было проброшено откуда-то ещё
Expect::exception(ValidationException::class)
    ->fromMethod(UserValidator::class, 'validate');

ExpectedException::withMessage

ExpectedException::withMessage(string $message): selfПроверяет точное сообщение исключения.

Повторный вызов заменяет предыдущее ожидание.

ExpectedException::withMessagePattern

ExpectedException::withMessagePattern(string $pattern): selfПроверяет, что сообщение соответствует регулярному выражению.

Повторный вызов заменяет предыдущее ожидание.

ExpectedException::withMessageContaining

ExpectedException::withMessageContaining(string $substring): selfПроверяет, что сообщение содержит указанную подстроку.

Можно вызывать несколько раз — сообщение должно содержать все указанные подстроки.

ExpectedException::withCode

ExpectedException::withCode(int|array $code): selfПроверяет код исключения. Можно передать одно значение или массив допустимых кодов.

Повторный вызов заменяет предыдущее ожидание.

ExpectedException::withoutPrevious

ExpectedException::withoutPrevious(): selfПроверяет, что у исключения нет предыдущего исключения.

ExpectedException::withPrevious

ExpectedException::withPrevious(string|\Throwable $classOrObject, ?callable $assertion = null): selfПроверяет наличие предыдущего исключения указанного типа.

Повторный вызов заменяет предыдущее ожидание.

$assertion -> Опциональный callback, получающий ExpectedException по предыдущей ошибке — это позволяет строить вложенные проверки с тем же API: проверить сообщение, код или даже его собственный withPrevious().
php
Expect::exception(PaymentException::class)
    ->withPrevious(
        GatewayException::class,
        fn (ExpectedException $previous) => $previous
            ->withCode(503)
            ->withMessageContaining('connection refused'),
    );

Все методы цепочки можно комбинировать в произвольном порядке, выстраивая точное описание ожидаемого исключения:

php
Expect::exception(PaymentException::class)
    ->fromMethod(PaymentGateway::class, 'charge')
    ->withMessageContaining('insufficient funds')
    ->withCode([402, 422])
    ->withPrevious(GatewayException::class);

Expect::notLeaks

Ожидает, что объекты будут освобождены из памяти после завершения теста.

Expect::notLeaks(object ...$objects): NotLeaks

Полезно, когда нужно убедиться, что сервис корректно освобождает ресурсы.

Примеры:

php
#[Test]
public function serviceReleasesResources(): void
{
    $connection = new Connection();
    $service = new Service($connection);

    Expect::notLeaks($connection, $service);

    $service->process();
    // После теста Testo проверит, что $connection и $service больше не удерживаются в памяти
}

Expect::leaks

Ожидает, что объекты останутся в памяти после завершения теста.

Expect::leaks(object ...$objects): Leaks

Полезно, чтобы убедиться, что кеш или другой механизм действительно удерживает объекты.

PHP может не собрать объекты, если тест завершается выбросом исключения. Также существуют известные проблемы со сборкой мусора на macOS.

Примеры:

php
#[Test]
public function cachePersistsObjects(): void
{
    $entity = new User();
    $cache->store($entity);

    Expect::leaks($entity);
    // После теста Testo проверит, что $entity всё ещё удерживается в памяти (кешем)
}