Утверждения (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.