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. Можем да използваме два варианта на шаблони: регулярни изрази или заместващи знаци.

Ако като $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)

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.