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::notContains ​

IterableType::notContains(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));

То же ожидание можно объявить атрибутом вместо вызова — удобно, когда весь тест сводится к «здесь должно быть исключение»:

#[ExpectException] ​

Атрибутная форма Expect::exception()Expect::exception(string|\Throwable $classOrObject, bool $same = false): ExpectedExceptionОжидает, что тест выбросит указанное исключение..

#[ExpectException(string $class)]

Компактная и декларативная альтернатива для частого случая, когда нужно проверить лишь тип исключения. Класс сверяется через instanceof, поэтому наследники тоже проходят проверку.

Атрибут принимает только класс. Для более тонких проверок — сообщения, кода или предыдущего исключения — вызывайте Expect::exception()Expect::exception(string|\Throwable $classOrObject, bool $same = false): ExpectedExceptionОжидает, что тест выбросит указанное исключение. в теле теста.

Параметры:

$class
Класс или интерфейс ожидаемого исключения.

Примеры:

php
use Testo\Assert\ExpectException;

#[Test]
#[ExpectException(\InvalidArgumentException::class)]
public function throwsOnInvalidInput(): void
{
    $service->process(null);
}

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

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 всё ещё удерживается в памяти (кешем)
}

Тесты без проверок ​

По умолчанию тест, который завершился, не записав ни одной проверки, помечается статусом Status::Risky\Testo\Core\Value\Status::Risky — Testo считает, что вы забыли что-то проверить. Но иногда тест действительно ничего не проверяет: ему достаточно убедиться, что вызов не выбросил исключение. Именно это и объявляет атрибут #[ExpectNoAssertions]#[ExpectNoAssertions]Объявляет, что тест намеренно не выполняет проверок..

#[ExpectNoAssertions] ​

Объявляет, что тест намеренно не выполняет проверок.

#[ExpectNoAssertions]

Это двусторонний контракт, который проверяется в конце прогона:

  • помеченный тест, не записавший ни одной проверки, остаётся в статусе Status::Passed\Testo\Core\Value\Status::Passed вместо того, чтобы стать рискованным;
  • помеченный тест, который всё же записал проверку, становится Status::Risky\Testo\Core\Value\Status::Risky — объявление больше не выполняется, а значит, атрибут устарел или применён по ошибке.

Ожидание исключения (через Expect::exception()Expect::exception(string|\Throwable $classOrObject, bool $same = false): ExpectedExceptionОжидает, что тест выбросит указанное исключение. или #[ExpectException]#[ExpectException(string $class)]Атрибутная форма Expect::exception()Expect::exception(string|\Throwable $classOrObject, bool $same = false): ExpectedExceptionExpects the test to throw the given exception..) само по себе является проверкой, поэтому сочетать его с этим атрибутом бессмысленно — тест получит статус Status::Risky\Testo\Core\Value\Status::Risky.

Примеры:

php
#[Test]
#[ExpectNoAssertions]
public function bootDoesNotThrow(): void
{
    // Успех — это просто дойти до конца без исключения
    $this->app->boot();
}