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 -> Причина провала теста.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Проверяет отсутствие данных.), так и в цепочках проверок:
Assert::same($user->role, 'admin', 'У пользователя должна быть роль admin');Цепочки проверок
Вместо десятков отдельных методов вроде assertStringContains(), assertArrayHasKey() и ещё двадцати с префиксом string*, Testo группирует проверки в типизированные цепочки.
Идея простая: метод в начале цепочки проверяет, что значение имеет нужный тип, а затем открывает доступ к специфичным для этого типа проверкам. Методы можно вызывать друг за другом:
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Примеры:
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Проверяет, что значение меньше или равно указанному.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 можно итерировать только один раз.
Примеры:
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Наследует все методы Assert::iterable()Assert::iterable(mixed $actual): IterableTypeПроверяет, что значение является iterable, и открывает проверки для коллекций. и добавляет проверки, специфичные для массивов.
Примеры:
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Примеры:
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::isPrimitive
JsonAbstract::isPrimitive(): JsonCommonПроверяет, что JSON представляет примитивное значение (строка, число, boolean, null).JsonAbstract::isStructure
JsonAbstract::isStructure(): JsonStructureПроверяет, что JSON представляет структуру (объект или массив).JsonAbstract::maxDepth
JsonAbstract::maxDepth(int $expected): staticПроверяет, что глубина вложенности 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.// Определение типа и проверка структуры
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считаются незаданными и не проверяются.
// проверяется только 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, применяется самая строгая проверка, доступная для переданного типа.
Примеры:
use Testo\Expect;
#[Test]
public function throwsOnInvalidInput(): void
{
// Достаточно instanceof — подойдёт InvalidArgumentException и любой его наследник
Expect::exception(\InvalidArgumentException::class);
$service->process(null);
}// Точное совпадение класса: наследники RuntimeException провалят проверку
Expect::exception(\RuntimeException::class, same: true);// Объект-образец: разом задаём ожидаемый класс, сообщение и код
Expect::exception(new PaymentException('insufficient funds', 402));С помощью цепочки методов можно уточнить, какое именно исключение ожидается:
ExpectedException::fromMethod
ExpectedException::fromMethod(string $class, string $method): selfПроверяет, что указанный метод присутствует в стеке вызовов исключения.Метод можно вызывать несколько раз — для прохождения проверки в стеке должны присутствовать все указанные методы.
Стек вызовов в исключении заполняется в момент его создания, а не выброса. Таким образом, мы проверяем именно место создания, а не проброс через throw.
// Убеждаемся, что исключение возникло именно в валидации,
// а не было проброшено откуда-то ещё
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().Expect::exception(PaymentException::class)
->withPrevious(
GatewayException::class,
fn (ExpectedException $previous) => $previous
->withCode(503)
->withMessageContaining('connection refused'),
);Все методы цепочки можно комбинировать в произвольном порядке, выстраивая точное описание ожидаемого исключения:
Expect::exception(PaymentException::class)
->fromMethod(PaymentGateway::class, 'charge')
->withMessageContaining('insufficient funds')
->withCode([402, 422])
->withPrevious(GatewayException::class);Expect::notLeaks
Ожидает, что объекты будут освобождены из памяти после завершения теста.
Expect::notLeaks(object ...$objects): NotLeaksПолезно, когда нужно убедиться, что сервис корректно освобождает ресурсы.
Примеры:
#[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.
Примеры:
#[Test]
public function cachePersistsObjects(): void
{
$entity = new User();
$cache->store($entity);
Expect::leaks($entity);
// После теста Testo проверит, что $entity всё ещё удерживается в памяти (кешем)
}