Aserce

Aserce se používají k potvrzení, že skutečná hodnota odpovídá očekávané hodnotě. Jde o metody třídy Tester\Assert.

Vybírejte co nejvhodnější aserce. Je lepší Assert::same($a, $b) než Assert::true($a === $b), protože při selhání zobrazí smysluplnou chybovou zprávu. Ve druhém případě pouze false should be true což nám o obsahu proměnných $a a $b nic neříká.

Většina assercí také může mít volitelnou popisku v parametru $description, která se zobrazí v chybové hlášce, pokud očekávání selže.

Příklady předpokládají vytvořený alias:

use Tester\Assert;

Assert::same($expected, $actual, string $description=null)

$expected musí být totožný s $actual. To samé jako PHP operátor ===.

Assert::notSame($expected, $actual, string $description=null)

Opak Assert::same(), tedy to samé jako PHP operátor !==.

Assert::equal($expected, $actual, string $description=null, bool $matchOrder=false, bool $matchIdentity=false)

$expected musí být stejný s $actual. Na rozdíl od Assert::same() se ignoruje identita objektů, pořadí dvojic klíčů ⇒ hodnota v polích a marginálně odlišná desetinná čísla, což lze změnit nastavením $matchIdentity a $matchOrder.

Následující případy jsou shodné z pohledu equal(), ale nikoliv same():

Assert::equal(0.3, 0.1 + 0.2);
Assert::equal($obj, clone $obj);
Assert::equal(
	['first' => 11, 'second' => 22],
	['second' => 22, 'first' => 11],
);

Ovšem pozor, pole [1, 2] a [2, 1] stejné nejsou, protože se liší jen pořadí hodnot, nikoliv dvojic klíč ⇒ hodnota. Pole [1, 2] lze zapsat také jako [0 => 1, 1 => 2] a za stejné se proto bude považovat [1 => 2, 0 => 1].

Dále lze v $expected použít tzv. očekávání.

Assert::notEqual($expected, $actual, string $description=null)

Opak Assert::equal().

Assert::contains($needle, string|array $actual, string $description=null)

Pokud je $actual řetězec, musí obsahovat podřetězec $needle. Pokud je pole, musí obsahovat prvek $needle (porovnává se striktně).

Assert::notContains($needle, string|array $actual, string $description=null)

Opak Assert::contains().

Assert::hasKey(string|int $needle, array $actual, string $description=null)

$actual musí být pole a musí obsahovat klíč $needle.

Assert::notHasKey(string|int $needle, array $actual, string $description=null)

$actual musí být pole a nesmí obsahovat klíč $needle.

Assert::true($value, string $description=null)

$value musí být true, tedy $value === true.

Assert::truthy($value, string $description=null)

$value musí být pravdivý, tedy splní podmínku if ($value) ....

Assert::false($value, string $description=null)

$value musí být false, tedy $value === false.

Assert::falsey($value, string $description=null)

$value musí být nepravdivý, tedy splní podmínku if (!$value) ....

Assert::null($value, string $description=null)

$value musí být null, tedy $value === null.

Assert::notNull($value, string $description=null)

$value nesmí být null, tedy $value !== null.

Assert::nan($value, string $description=null)

$value musí být Not a Number. Pro testování NAN hodnoty používejte vyhradně Assert::nan(). Hodnota NAN je velmi specifická a aserce Assert::same() nebo Assert::equal() mohou fungovat neočekávaně.

Assert::count($count, Countable|array $value, string $description=null)

Počet prvků ve $value musí být $count. Tedy to samé jako count($value) === $count.

Assert::type(string|object $type, $value, string $description=null)

$value musí být daného typu. Jako $type můžeme použít řetězec:

  • array
  • list – pole indexované podle vzestupné řady numerických klíčů od nuly
  • bool
  • callable
  • float
  • int
  • null
  • object
  • resource
  • scalar
  • string
  • název třídy nebo přímo objekt, potom musí být $value instanceof $type

Assert::exception(callable $callable, string $class, string $message=null, $code=null)

Při zavolání $callable musí být vyhozena výjimka třídy $class. Pokud uvedeme $message, musí odpovídat vzoru i zpráva výjimky a pokud uvedeme $code, musí se striktně shodovat i kódy.

Následující test selže, protože neodpovídá zpráva výjimky:

Assert::exception(
	fn() => throw new App\InvalidValueException('Zero value'),
	App\InvalidValueException::class,
	'Value is to low',
);

Assert::exception() vrací vyhozenou výjimku, lze tak otestovat i výjimku zahnízděnou.

$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)

Kontroluje, že funkce $callable vygenerovala očekávané chyby (tj. varování, notices atd). Jako $type uvedeme jednu z konstant E_..., tedy například E_WARNING. A pokud uvedeme $message, musí odpovídat vzoru i chybová zpráva. Například:

Assert::error(
	fn() => $i++,
	E_NOTICE,
	'Undefined variable: i',
);

Pokud callback vygeneruje více chyb, musíme je všechny očekávat v přesném pořadí. V takovém případě předáme v $type pole:

Assert::error(function () {
	$a++;
	$b++;
}, [
	[E_NOTICE, 'Undefined variable: a'],
	[E_NOTICE, 'Undefined variable: b'],
]);

Pokud jako $type uvedete název třídy, chová se stejně jako Assert::exception().

Assert::noError(callable $callable)

Kontroluje, že funkce $callable nevygenerovala žádné varování, chybu nebo výjimku. Hodí se pro testování kousků kódu, kde není žádná další aserce.

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

$actual musí vyhovět vzoru $pattern. Můžeme použít dvě varianty vzorů: regulární výrazy nebo zástupné znaky.

Pokud jako $pattern předáme regulární výraz, k jeho ohraničení musíme použít ~ nebo #, jiné oddělovače nejsou podporovány. Například test, kdy $var musí obsahovat pouze hexadecimální číslice:

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

Druhá varianta je podobná běžnému porovnání řetězců, ale v $pattern můžeme použít různé zástupné znaky:

  • %a% jeden nebo více znaků, kromě znaků konce řádku
  • %a?% žádný nebo více znaků, kromě znaků konce řádku
  • %A% jeden nebo více znaků, včetně znaků konce řádku
  • %A?% žádný nebo více znaků, včetně znaků konce řádku
  • %s% jeden nebo více bílých znaků, kromě znaků konce řádku
  • %s?% žádný nebo více bílých znaků, kromě znaků konce řádku
  • %S% jeden nebo více znaků, kromě bílých znaků
  • %S?% žádný nebo více znaků, kromě bílých znaků
  • %c% jakýkoli jeden znak, kromě znaku konce řádku
  • %d% jedna nebo více číslic
  • %d?% žádná nebo více číslic
  • %i% znaménková celočíselná hodnota
  • %f% číslo s desetinnou čárkou
  • %h% jedna nebo více hexadecimálních číslic
  • %w% jeden nebo více alfanumerických znaků
  • %% znak %

Příklady:

# Opět test na hexadecimální číslo
Assert::match('%h%', $var);

# Zobecnění cesty k souboru a čísla řádky
Assert::match('Error in file %a% on line %i%', $errorMessage);

Assert::matchFile(string $file, $actual, string $description=null)

Aserce je totožná s Assert::match(), ale vzor se načítá ze souboru $file. To je užitečné pro testování velmi dlouhých řetězců. Soubor s testem zůstane přehledný.

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

Tato aserce vždy selže. Někdy se to prostě hodí. Volitelně můžeme uvést i očekávanou a aktuální hodnotu.

Očekávání

Když chceme porovnat složitější struktury s nekonstantními prvky, nemusí být výše uvedené aserce dostatečné. Například testujeme metodu, která vytváří nového uživatele a vrací jeho atributy jako pole. Hodnotu hashe hesla neznáme, ale víme, to že musí být hexadecimální řetězec. A o dalším prvku víme jen, že to musí být objekt DateTime.

V těchto situacích můžeme použít Tester\Expect uvnitř $expected parametru metod Assert::equal() a Assert::notEqual(), pomocí kterých lze strukturu snadno popsat.

use Tester\Expect;

Assert::equal([
	'id' => Expect::type('int'),                   # očekáváme celé číslo
	'username' => 'milo',
	'password' => Expect::match('%h%'),            # očekáváme řetězec vyhovující vzoru
	'created_at' => Expect::type(DateTime::class), # očekáváme instanci třídy
], User::create(123, 'milo', 'RandomPaSsWoRd'));

S Expect můžeme provádět téměř stejné aserce jako s Assert. Tedy jsou nám k dispozici metody Expect::same(), Expect::match(), Expect::count() atd. Navíc je můžeme zřetězit:

Expect::type(MyIterator::class)->andCount(5);  # očekáváme MyIterator a počet prvků 5

Anebo můžeme psát vlastní handlery asercí.

Expect::that(function ($value) {
	# vrátíme false, pokud očekávání selže
});

Zkoumání chybných asercí

Když aserce selže, Tester vypíše, v čem je chyba. Pokud porovnáváme složitější struktury, Tester vytvoří dumpy porovnávaných hodnot a uloží je do adresáře output. Například při selhání smyšleného testu Arrays.recursive.phpt budou dumpy uloženy následovně:

app/
└── tests/
	├── output/
	│   ├── Arrays.recursive.actual    # aktuální hodnota
	│   └── Arrays.recursive.expected  # očekávaná hodnota
	│
	└── Arrays.recursive.phpt          # selhávající test

Název adresáře můžeme změnit přes Tester\Dumper::$dumpDir.