Утверждения
***********
.[perex]
Утверждения служат для подтверждения того, что фактическое значение соответствует ожидаемому. Это методы класса `Tester\Assert`.
Выбирайте наиболее подходящие утверждения. `Assert::same($a, $b)` лучше, чем `Assert::true($a === $b)`, потому что при провале выводит осмысленное сообщение об ошибке. Во втором случае мы получим только `false should be true`, что ничего не говорит о содержимом переменных `$a` и `$b`.
У большинства утверждений может быть и необязательное описание в параметре `$description`, которое выводится в сообщении об ошибке, если ожидание не оправдается.
Примеры предполагают, что создан такой псевдоним:
```php
use Tester\Assert;
```
Assert::same($expected, $actual, ?string $description=null) .[method]
---------------------------------------------------------------------
`$expected` должно быть идентично `$actual`. То же самое, что оператор PHP `===`.
Assert::notSame($expected, $actual, ?string $description=null) .[method]
------------------------------------------------------------------------
Противоположность `Assert::same()`, то есть то же самое, что оператор PHP `!==`.
Assert::equal($expected, $actual, ?string $description=null, bool $matchOrder=false, bool $matchIdentity=false) .[method]
-------------------------------------------------------------------------------------------------------------------------
`$expected` должно быть равно `$actual`. В отличие от `Assert::same()`, игнорируются идентичность объектов, порядок пар ключ => значение в массивах и незначительно различающиеся дробные числа, что можно изменить заданием `$matchIdentity` и `$matchOrder`.
С точки зрения `equal()` следующие случаи равны, а с точки зрения `same()` - нет:
```php
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) .[method]
-------------------------------------------------------------------------
Противоположность `Assert::equal()`.
Assert::contains($needle, string|array $actual, ?string $description=null) .[method]
------------------------------------------------------------------------------------
Если `$actual` - строка, она должна содержать подстроку `$needle`. Если это массив, он должен содержать элемент `$needle` (сравнение строгое).
Assert::notContains($needle, string|array $actual, ?string $description=null) .[method]
---------------------------------------------------------------------------------------
Противоположность `Assert::contains()`.
Assert::hasKey(string|int $needle, array $actual, ?string $description=null) .[method]{data-version:2.4.0}
----------------------------------------------------------------------------------------------------------
`$actual` должно быть массивом и должно содержать ключ `$needle`.
Assert::hasNotKey(string|int $needle, array $actual, ?string $description=null) .[method]{data-version:2.4.0}
-------------------------------------------------------------------------------------------------------------
`$actual` должно быть массивом и не должно содержать ключ `$needle`.
Assert::true($value, ?string $description=null) .[method]
---------------------------------------------------------
`$value` должно быть `true`, то есть `$value === true`.
Assert::truthy($value, ?string $description=null) .[method]
-----------------------------------------------------------
`$value` должно быть истинным, то есть удовлетворять условию `if ($value) ...`.
Assert::false($value, ?string $description=null) .[method]
----------------------------------------------------------
`$value` должно быть `false`, то есть `$value === false`.
Assert::falsey($value, ?string $description=null) .[method]
-----------------------------------------------------------
`$value` должно быть ложным, то есть удовлетворять условию `if (!$value) ...`.
Assert::null($value, ?string $description=null) .[method]
---------------------------------------------------------
`$value` должно быть `null`, то есть `$value === null`.
Assert::notNull($value, ?string $description=null) .[method]
------------------------------------------------------------
`$value` не должно быть `null`, то есть `$value !== null`.
Assert::nan($value, ?string $description=null) .[method]
--------------------------------------------------------
`$value` должно быть Not a Number. Для проверки значений NAN используйте исключительно `Assert::nan()`. Значение NAN очень специфично, и утверждения вроде `Assert::same()` или `Assert::equal()` могут вести себя неожиданно.
Assert::count($count, Countable|array $value, ?string $description=null) .[method]
----------------------------------------------------------------------------------
Количество элементов в `$value` должно быть равно `$count`. То же самое, что `count($value) === $count`.
Assert::type(string|object $type, $value, ?string $description=null) .[method]
------------------------------------------------------------------------------
`$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) .[method]
-------------------------------------------------------------------------------------------------
При вызове `$callable` должно быть выброшено исключение класса `$class`. Если мы укажем `$message`, сообщение исключения должно ещё и [соответствовать образцу |#Assert::match()]. А если мы укажем `$code`, коды тоже должны строго совпадать.
Например, следующий тест провалится, потому что сообщение исключения не совпадает:
```php
Assert::exception(
fn() => throw new App\InvalidValueException('Zero value'),
App\InvalidValueException::class,
'Value is too low',
);
```
`Assert::exception()` возвращает выброшенное исключение, что позволяет проверить и вложенное исключение.
```php
$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) .[method]
----------------------------------------------------------------------------------------
Проверяет, что функция `$callable` породила ожидаемые ошибки (то есть warning, notice и т. п.). В качестве `$type` укажите одну из констант `E_...`, например `E_WARNING`. А если мы укажем `$message`, сообщение об ошибке должно ещё и [соответствовать образцу |#Assert::match()]. Например:
```php
Assert::error(
fn() => $i++,
E_NOTICE,
'Undefined variable: i',
);
```
Если callback порождает больше ошибок, мы должны ожидать их все в точном порядке. В этом случае передайте в `$type` массив:
```php
Assert::error(function () {
$a++;
$b++;
}, [
[E_NOTICE, 'Undefined variable: a'],
[E_NOTICE, 'Undefined variable: b'],
]);
```
.[note]
Если в качестве `$type` указать имя класса, поведение будет таким же, как у `Assert::exception()`.
Assert::noError(callable $callable) .[method]
---------------------------------------------
Проверяет, что функция `$callable` не породила никакого warning, ошибки или исключения. Это удобно для проверки кусков кода, где никакого другого утверждения нет.
Assert::match(string $pattern, $actual, ?string $description=null) .[method]
----------------------------------------------------------------------------
`$actual` должно соответствовать образцу `$pattern`. Мы можем использовать два варианта образцов: регулярные выражения или подстановочные знаки.
Если мы передадим в `$pattern` регулярное выражение, для его ограничения нужно использовать `~` или `#`. Другие ограничители не поддерживаются. Например, тест, в котором `$var` должна содержать только шестнадцатеричные цифры:
```php
Assert::match('#^[0-9a-f]+$#i', $var);
```
Второй вариант похож на сравнение обычных строк, но в `$pattern` мы можем использовать разные подстановочные знаки:
- `%a%` один или более любых символов, кроме символов конца строки
- `%a?%` ноль или более любых символов, кроме символов конца строки
- `%A%` один или более любых символов, включая символы конца строки
- `%A?%` ноль или более любых символов, включая символы конца строки
- `%s%` один или более пробельных символов, кроме символов конца строки
- `%s?%` ноль или более пробельных символов, кроме символов конца строки
- `%S%` один или более символов, кроме пробельных
- `%S?%` ноль или более символов, кроме пробельных
- `%c%` один любой символ (кроме конца строки)
- `%d%` одна или более цифр
- `%d?%` ноль или более цифр
- `%i%` целое число со знаком
- `%f%` число с плавающей точкой
- `%h%` одна или более шестнадцатеричных цифр
- `%w%` один или более буквенно-цифровых символов
- `%ds%` разделитель каталогов (`/` или `\`)
- `%%` один символ %
Примеры:
```php
# Снова проверка шестнадцатеричного числа
Assert::match('%h%', $var);
# Обобщение пути к файлу и номера строки
Assert::match('Error in file %a% on line %i%', $errorMessage);
```
Assert::notMatch(string $pattern, $actual, ?string $description=null) .[method]{data-version:2.5.6}
---------------------------------------------------------------------------------------------------
Противоположность `Assert::match()`.
Assert::matchFile(string $file, $actual, ?string $description=null) .[method]
-----------------------------------------------------------------------------
Это утверждение идентично [#Assert::match()], но образец загружается из файла `$file`. Это удобно для проверки очень длинных строк. Файл теста остаётся понятным.
Assert::fail(string $message, $actual=null, $expected=null) .[method]
---------------------------------------------------------------------
Это утверждение проваливается всегда. Иногда это просто удобно. Необязательно можно указать ожидаемое и фактическое значение.
Ожидания
--------
Когда мы хотим сравнить более сложные структуры с непостоянными элементами, упомянутых выше утверждений может не хватить. Например, мы тестируем метод, который создаёт нового пользователя и возвращает его атрибуты массивом. Значения хеша пароля мы не знаем, но знаем, что это должна быть шестнадцатеричная строка. А о следующем элементе мы знаем только, что это должен быть объект `DateTime`.
В таких ситуациях внутри параметра `$expected` методов `Assert::equal()` и `Assert::notEqual()` мы можем использовать `Tester\Expect`, с помощью которого структуру легко описать.
```php
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()` и т. д. Кроме того, их можно объединять в цепочку:
```php
Expect::type(MyIterator::class)->andCount(5); # ожидаем MyIterator и количество элементов 5
```
Как вариант, мы можем написать собственные обработчики утверждений.
```php
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`.
Утверждения
Утверждения служат для подтверждения того, что фактическое
значение соответствует ожидаемому. Это методы класса 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::hasNotKey (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 too 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 породила ожидаемые ошибки (то
есть warning, notice и т. п.). В качестве $type укажите одну из констант
E_..., например E_WARNING. А если мы укажем $message,
сообщение об ошибке должно ещё и соответствовать
образцу. Например:
Assert::error(
fn() => $i++,
E_NOTICE,
'Undefined variable: i',
);
Если callback порождает больше ошибок, мы должны ожидать их все в точном
порядке. В этом случае передайте в $type массив:
Assert::error(function () {
$a++;
$b++;
}, [
[E_NOTICE, 'Undefined variable: a'],
[E_NOTICE, 'Undefined variable: b'],
]);
Если в качестве $type указать имя класса, поведение будет
таким же, как у Assert::exception().
Assert::noError (callable
$callable)
Проверяет, что функция $callable не породила никакого warning, ошибки
или исключения. Это удобно для проверки кусков кода, где никакого
другого утверждения нет.
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% один или более буквенно-цифровых символов
%ds% разделитель каталогов (/ или \)
%% один символ %
Примеры:
# Снова проверка шестнадцатеричного числа
Assert::match('%h%', $var);
# Обобщение пути к файлу и номера строки
Assert::match('Error in file %a% on line %i%', $errorMessage);
Assert::notMatch (string
$pattern, $actual, ?string $description=null)
Противоположность Assert::match().
Assert::matchFile (string $file, $actual,
?string $description=null)
Это утверждение идентично Assert::match(), но образец
загружается из файла $file. Это удобно для проверки очень длинных
строк. Файл теста остаётся понятным.
Assert::fail (string $message, $actual=null,
$expected=null)
Это утверждение проваливается всегда. Иногда это просто удобно.
Необязательно можно указать ожидаемое и фактическое значение.
Ожидания
Когда мы хотим сравнить более сложные структуры с непостоянными
элементами, упомянутых выше утверждений может не хватить. Например, мы
тестируем метод, который создаёт нового пользователя и возвращает его
атрибуты массивом. Значения хеша пароля мы не знаем, но знаем, что это
должна быть шестнадцатеричная строка. А о следующем элементе мы знаем
только, что это должен быть объект DateTime.
В таких ситуациях внутри параметра $expected методов
Assert::equal() и Assert::notEqual() мы можем использовать
Tester\Expect, с помощью которого структуру легко описать.
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.