Утверждения (Assert)
Утверждения используются для подтверждения того, что
фактическое значение соответствует ожидаемому значению. Это методы
класса Tester\Assert
.
Выбирайте наиболее подходящие утверждения. Лучше Assert::same($a, $b)
,
чем Assert::true($a === $b)
, потому что при сбое оно отобразит осмысленное
сообщение об ошибке. Во втором случае только false should be true
, что
ничего не говорит нам о содержимом переменных $a
и $b
.
Большинство утверждений также могут иметь необязательное описание в
параметре $description
, которое будет отображено в сообщении об
ошибке, если ожидание не оправдается.
Примеры предполагают созданный псевдоним:
use Tester\Assert;
Assert::same ($expected, $actual, ?string $description=null)
$expected
должен быть идентичен $actual
. То же самое, что и PHP
оператор ===
.
Assert::notSame ($expected, $actual, ?string $description=null)
Противоположность Assert::same()
, то есть то же самое, что и PHP
оператор !==
.
Assert::equal ($expected, $actual, ?string $description=null, bool $matchOrder=false, bool $matchIdentity=false)
$expected
должен быть равен $actual
. В отличие от
Assert::same()
, игнорируется идентичность объектов, порядок пар ключ
⇒ значение в массивах и незначительно отличающиеся десятичные числа,
что можно изменить настройкой $matchIdentity
и $matchOrder
.
Следующие случаи считаются равными с точки зрения equal()
, но не
same()
:
Assert::equal(0.3, 0.1 + 0.2);
Assert::equal($obj, clone $obj);
Assert::equal(
['first' => 11, 'second' => 22],
['second' => 22, 'first' => 11],
);
Однако будьте осторожны, массивы [1, 2]
и [2, 1]
не равны,
потому что отличается только порядок значений, а не пар ключ ⇒
значение. Массив [1, 2]
можно также записать как
[0 => 1, 1 => 2]
, и поэтому равным будет считаться
[1 => 2, 0 => 1]
.
Далее в $expected
можно использовать так называемые Ожидания.
Assert::notEqual ($expected, $actual, ?string $description=null)
Противоположность Assert::equal()
.
Assert::contains ($needle, string|array $actual, ?string $description=null)
Если $actual
— строка, она должна содержать подстроку
$needle
. Если это массив, он должен содержать элемент $needle
(сравнивается строго).
Assert::notContains ($needle, string|array $actual, ?string $description=null)
Противоположность Assert::contains()
.
Assert::hasKey (string|int $needle, array $actual, ?string $description=null)
$actual
должен быть массивом и должен содержать ключ
$needle
.
Assert::notHasKey (string|int $needle, array $actual, ?string $description=null)
$actual
должен быть массивом и не должен содержать ключ
$needle
.
Assert::true ($value, ?string $description=null)
$value
должен быть true
, то есть $value === true
.
Assert::truthy ($value, ?string $description=null)
$value
должен быть истинным, то есть выполнит условие
if ($value) ...
.
Assert::false ($value, ?string $description=null)
$value
должен быть false
, то есть $value === false
.
Assert::falsey ($value, ?string $description=null)
$value
должен быть ложным, то есть выполнит условие
if (!$value) ...
.
Assert::null ($value, ?string $description=null)
$value
должен быть null
, то есть $value === null
.
Assert::notNull ($value, ?string $description=null)
$value
не должен быть null
, то есть $value !== null
.
Assert::nan ($value, ?string $description=null)
$value
должен быть Not a Number. Для тестирования значения NAN
используйте исключительно Assert::nan()
. Значение NAN очень
специфично, и утверждения Assert::same()
или Assert::equal()
могут
работать неожиданно.
Assert::count ($count, Countable|array $value, ?string $description=null)
Количество элементов в $value
должно быть $count
. То есть то
же самое, что и count($value) === $count
.
Assert::type (string|object $type, $value, ?string $description=null)
$value
должен быть данного типа. В качестве $type
мы можем
использовать строку:
array
list
– массив, индексированный по возрастающей последовательности числовых ключей от нуляbool
callable
float
int
null
object
resource
scalar
string
- имя класса или непосредственно объект, тогда
$value
должен бытьinstanceof $type
Assert::exception (callable $callable, string $class, ?string $message=null, $code=null)
При вызове $callable
должно быть выброшено исключение класса
$class
. Если мы укажем $message
, сообщение исключения также
должно соответствовать шаблону, и если мы укажем
$code
, коды также должны строго совпадать.
Следующий тест не пройдет, так как сообщение исключения не соответствует:
Assert::exception(
fn() => throw new App\InvalidValueException('Zero value'),
App\InvalidValueException::class,
'Value is to low',
);
Assert::exception()
возвращает выброшенное исключение, так что можно
протестировать и вложенное исключение.
$e = Assert::exception(
fn() => throw new MyException('Something is wrong', 0, new RuntimeException),
MyException::class,
'Something is wrong',
);
Assert::type(RuntimeException::class, $e->getPrevious());
Assert::error (string $callable, int|string|array $type, ?string $message=null)
Проверяет, что функция $callable
сгенерировала ожидаемые ошибки
(т.е. предупреждения, уведомления и т.д.). В качестве $type
укажем
одну из констант E_...
, то есть, например, E_WARNING
. И если мы
укажем $message
, сообщение об ошибке также должно соответствовать шаблону. Например:
Assert::error(
fn() => $i++,
E_NOTICE,
'Undefined variable: i',
);
Если обратный вызов генерирует несколько ошибок, мы должны ожидать
их все в точном порядке. В таком случае передадим в $type
массив:
Assert::error(function () {
$a++;
$b++;
}, [
[E_NOTICE, 'Undefined variable: a'],
[E_NOTICE, 'Undefined variable: b'],
]);
Если в качестве $type
вы укажете имя класса, он ведет себя
так же, как Assert::exception()
.
Assert::noError (callable $callable)
Проверяет, что функция $callable
не сгенерировала никаких
предупреждений, ошибок или исключений. Полезно для тестирования
фрагментов кода, где нет других утверждений.
Assert::match (string $pattern, $actual, ?string $description=null)
$actual
должен соответствовать шаблону $pattern
. Мы можем
использовать два варианта шаблонов: регулярные выражения или
подстановочные знаки.
Если в качестве $pattern
мы передаем регулярное выражение, для его
ограничения мы должны использовать ~
или #
, другие
разделители не поддерживаются. Например, тест, когда $var
должен
содержать только шестнадцатеричные цифры:
Assert::match('#^[0-9a-f]$#i', $var);
Второй вариант похож на обычное сравнение строк, но в $pattern
мы
можем использовать различные подстановочные знаки:
%a%
один или несколько символов, кроме символов конца строки%a?%
ноль или несколько символов, кроме символов конца строки%A%
один или несколько символов, включая символы конца строки%A?%
ноль или несколько символов, включая символы конца строки%s%
один или несколько пробельных символов, кроме символов конца строки%s?%
ноль или несколько пробельных символов, кроме символов конца строки%S%
один или несколько символов, кроме пробельных символов%S?%
ноль или несколько символов, кроме пробельных символов%c%
любой один символ, кроме символа конца строки%d%
одна или несколько цифр%d?%
ноль или несколько цифр%i%
знаковое целочисленное значение%f%
число с плавающей точкой%h%
одна или несколько шестнадцатеричных цифр%w%
один или несколько буквенно-цифровых символов%%
символ %
Примеры:
# Снова тест на шестнадцатеричное число
Assert::match('%h%', $var);
# Обобщение пути к файлу и номера строки
Assert::match('Error in file %a% on line %i%', $errorMessage);
Assert::matchFile (string $file, $actual, ?string $description=null)
Утверждение идентично Assert::match(), но шаблон
загружается из файла $file
. Это полезно для тестирования очень
длинных строк. Файл с тестом останется читаемым.
Assert::fail (string $message, $actual=null, $expected=null)
Это утверждение всегда не выполняется. Иногда это просто полезно. Опционально мы можем указать ожидаемое и фактическое значение.
Ожидания
Когда мы хотим сравнить сложные структуры с неконстантными
элементами, вышеуказанных утверждений может быть недостаточно.
Например, мы тестируем метод, который создает нового пользователя и
возвращает его атрибуты в виде массива. Значение хеша пароля мы не
знаем, но знаем, что это должна быть шестнадцатеричная строка. А о
другом элементе мы знаем только то, что это должен быть объект
DateTime
.
В этих ситуациях мы можем использовать Tester\Expect
внутри
параметра $expected
методов Assert::equal()
и Assert::notEqual()
, с
помощью которых можно легко описать структуру.
use Tester\Expect;
Assert::equal([
'id' => Expect::type('int'), # ожидаем целое число
'username' => 'milo',
'password' => Expect::match('%h%'), # ожидаем строку, соответствующую шаблону
'created_at' => Expect::type(DateTime::class), # ожидаем экземпляр класса
], User::create(123, 'milo', 'RandomPaSsWoRd'));
С Expect
мы можем выполнять почти те же утверждения, что и с
Assert
. То есть нам доступны методы Expect::same()
, Expect::match()
,
Expect::count()
и т.д. Кроме того, их можно объединять в цепочку:
Expect::type(MyIterator::class)->andCount(5); # ожидаем MyIterator и количество элементов 5
Или мы можем писать собственные обработчики утверждений.
Expect::that(function ($value) {
# вернем false, если ожидание не оправдается
});
Исследование ошибочных утверждений
Когда утверждение не выполняется, Tester выводит, в чем ошибка. Если мы
сравниваем сложные структуры, Tester создает дампы сравниваемых значений
и сохраняет их в каталоге output
. Например, при сбое вымышленного
теста Arrays.recursive.phpt
дампы будут сохранены следующим образом:
app/
└── tests/
├── output/
│ ├── Arrays.recursive.actual # фактическое значение
│ └── Arrays.recursive.expected # ожидаемое значение
│
└── Arrays.recursive.phpt # неработающий тест
Название каталога можно изменить через Tester\Dumper::$dumpDir
.