Assertion

Assertion використовуються для підтвердження того, що фактичне значення відповідає очікуваному значенню. Це методи класу Tester\Assert.

Вибирайте найбільш підходящі assertion. Краще Assert::same($a, $b), ніж Assert::true($a === $b), оскільки при невдачі вона відобразить змістовне повідомлення про помилку. У другому випадку лише false should be true, що нічого не говорить нам про вміст змінних $a та $b.

Більшість assertion також можуть мати необов'язковий опис у параметрі $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 є дуже специфічним, і assertion 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',
);

Якщо 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 не згенерувала жодного попередження, помилки або винятку. Корисно для тестування фрагментів коду, де немає жодного іншого assertion.

Assert::match (string $pattern, $actual, ?string $description=null)

$actual має відповідати патерну $pattern. Ми можемо використовувати два варіанти патернів: регулярні вирази або placeholder'и/wildcard'и.

Якщо як $pattern передамо регулярний вираз, для його розділення ми повинні використовувати ~ або #, інші роздільники не підтримуються. Наприклад, тест, коли $var має містити лише шістнадцяткові цифри:

Assert::match('#^[0-9a-f]$#i', $var);

Другий варіант схожий на звичайне порівняння рядків, але в $pattern ми можемо використовувати різні placeholder'и/wildcard'и:

  • %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)

Assertion тотожна Assert::match(), але патерн завантажується з файлу $file. Це корисно для тестування дуже довгих рядків. Файл з тестом залишиться зрозумілим.

Assert::fail (string $message, $actual=null, $expected=null)

Ця assertion завжди зазнає невдачі. Іноді це просто корисно. За бажанням можемо вказати й очікуване та фактичне значення.

Очікування

Коли ми хочемо порівняти складніші структури з неконстантними елементами, вищезазначених assertion може бути недостатньо. Наприклад, тестуємо метод, який створює нового користувача і повертає його атрибути як масив. Значення хешу пароля ми не знаємо, але знаємо, що це має бути шістнадцятковий рядок. А про інший елемент знаємо лише, що це має бути об'єкт 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 ми можемо виконувати майже ті самі assertion, що й з Assert. Тобто нам доступні методи Expect::same(), Expect::match(), Expect::count() тощо. Крім того, їх можна об'єднувати в ланцюжок:

Expect::type(MyIterator::class)->andCount(5);  # очікуємо MyIterator та кількість елементів 5

Або можемо писати власні обробники assertion.

Expect::that(function ($value) {
	# повернемо false, якщо очікування не виправдається
});

Дослідження помилкових assertion

Коли assertion зазнає невдачі, Tester виводить, у чому полягає помилка. Якщо порівнюємо складніші структури, Tester створює дампи порівнюваних значень і зберігає їх у директорії output. Наприклад, при невдачі вигаданого тесту Arrays.recursive.phpt дампи будуть збережені наступним чином:

app/
└── tests/
	├── output/
	│   ├── Arrays.recursive.actual    # фактичне значення
	│   └── Arrays.recursive.expected  # очікуване значення
	│
	└── Arrays.recursive.phpt          # тест, що зазнав невдачі

Назву директорії можемо змінити через Tester\Dumper::$dumpDir.